Passer au contenu principal

Les collections sont un vieux serpent de mer avec Hibernate et doivent être traitées très spécifiquement, contrairement aux autres entités. En fait leur mauvaise compréhension est une source majeure de bugs aussi nous creusons le sujet ici.

Hibernate implémente un mécanisme de dirty checking qui permet de savoir lorsqu’un commit de transaction a lieu quelles sont les entités à mettre à jour en base. Ce mécanisme repose sur le traditionnel equals() de Java, et si le equals() ne correspond pas à ce qui était dans le cache d’Hibernate les entités managées par Hibernate (autrement dit en état persistant pour les puristes) sont mises à jour en base. Ceci explique notamment qu’il n’y a absolument pas besoin de lancer un saveOrUpdate sur une entité déjà managée par Hibernate lorsque vous la modifiez et que vous êtes dans un contexte transactionnel.

Malheureusement pour nous, il existe un vilain petit canard dans tout ce beau monde, à savoir les collections, où justement tout est géré par référence.

La première règle : évitez au maximum de mapper les collections

Les mappings les plus courants de collections passent par un @OneToMany du côté du propriétaire de la liaison et un @ManyToOne de l’autre côté, ce dernier définissant la colonne de table faisant la liaison. Sauf que bien souvent il n’y a aucune raison de rendre la liaison navigable dans les deux sens, et généralement c’est le sens ManyToOne qui a plus de raisons de voir l’autre côté de la liaison que l’inverse. Donc dans ce cas le fait d’implémenter l’autre sens risque surtout d’amener des soucis comme le fameux problème N + 1 d’Hibernate, qui se manifeste quand on voit des centaines de selects identiques apparaître en base.

Autrement dit avant d’implémenter un @OneToMany réfléchissez bien au sens dans lequel votre liaison doit être navigable (et que ce n’est pas pratique d’implémenter un findByXXX sur le DAO de l’autre côté de la liaison), vous éviterez bien des soucis.

La deuxième règle : évitez le cascading

La deuxième règle est aussi en lien avec la première, à savoir que si vraiment vous avez besoin de mapper une collection, il vaut mieux dans la mesure du possible éviter le cascading. Ce dernier est un mécanisme bien pratique qui permet, lorsqu’on met à jour l’entité parent, de mettre à jour les entités enfant de ce parent. Tant qu’on n’utilise pas les collections c’est très pratique, maintenant avec ces dernières le problème est qu’il faut mettre à jour le contenu des objets de la collection et non les objets eux-mêmes. Autrement dit il est impossible de remplacer dans une collection une instance d’objet en état persistant avec une instance en état détaché par exemple (autrement dit non référencé par Hibernate), sinon Hibernate ne pourra pas faire la sauvegarde.

Alors vous me direz qu’il serait possible de remplacer directement la collection en elle-même, par exemple en passant par un setter. Sauf qu’en fait ça ne marche pas non plus, car Hibernate considèrera alors que toutes les entités stockées dans la collection sont… nouvelles ! Et là à vous les joies des doublons et autres. Pour vous en convaincre mettez un point d’arrêt dans votre code au moment de sauvegarder un objet contenant une collection @OneToMany stockée dans une instance de java.util.Set, et vous constaterez que votre joli HashSet est en fait un org.hibernate.collections.PersistentSet !!!

Vous voulez quand même gérer le cascading

Bon alors comme vous insistez vraiment, je vais vous fournir l’algorithme général de merge de collections en Hibernate, à savoir :

  1. Commencez par faire un merge de toutes les entités déjà en base. Elles sont simple à repérer en faisant une liaison par la clef primaire de la table. Faites bien attention de mettre à jour le contenu des entités et pas les références des entités en elle-mêmes.
  2. Ensuite ajouter dans la collection d’objets managés les objets qui n’ont pas été mergés.
  3. Enfin, pour chaque objet que vous souhaitez retirer de la collection cible, il faut à la fois le retirer de la collection et invoquer un session.delete().

Alors oui c’est horriblement complexe, mais comme je suis sympa j’ai créé un petit projet sur GitHub qui contient le nécessaire pour faire ça sans douleur. Regardez la classe GojulHibernateCollectionsMergeTool (et oui le préfixe Gojul sert avant tout à éviter les collisions de nommage car je suis convaincu d’être le seul imbécile à avoir eu l’idée de nommer ses classes de la sorte… :D). Pour l’instant la classe ne fonctionne que sur des instances de java.util.Set mais si à l’avenir le besoin apparaît elle pourrait fonctionner aussi sur des instances de java.util.List.

Cet article vous a plu ? Vous aimerez sûrement aussi :

Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.

Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.

Son Twitter Son LinkedIn

Laisser un commentaire