Hibernate Survival Guide Partie 2
Un article de ODcWiki.
| |
Ceci est la seconde pièce de la série d’articles qui explique comment fonctionne Hibernate et comment l’utiliser au mieux. Le précédant article expliquait ce qu’était le cache de premier niveau et combien il était fondamental. Cet article s’attaque à un autre point épineux : la gestion des liens inverses dans une relation.
Sommaire |
Un peu de vocabulaire
Afin de mieux nous comprendre dans la suite de cet article, posons le vocabulaire suivant :
- Un lien est une information qui permet à partir d’une entité A d’atteindre le ou les entités B liées à A pour une raison donnée (A est propriétaire de B par exemple). Un lien est un attribut appartenant à A. Ce peut être soit une simple référence vers un B (on parlera alors de lien « unique »), soit une collection de B (on parlera alors de lien « multiple »). C’est une notion qui est directement implémentable en Java. Mais pas en Base de données.
- Une relation est un lien libre (c'est-à-dire non asservi) ou un couple de liens asservis. Un lien est asservi à un autre, si leurs valeurs doivent être maintenues en cohérence. Par exemple si A désigne B par le premier lien du couple, il faut que B désigne A par l’autre lien asservi. Une relation n’appartient ni à A, ni à B puisqu’en fait, elle est située entre les deux entités. Une relation est matérialisée en Base de données par une clé étrangère. Elle n’a pas de représentation directe en Java. Il existe trois types de relations : les relations 1 pour 1, 1 pour N et N pour N.
- Un lien L1 et dit inverse de L2 si L1 et L2 sont asservis, c'est-à-dire s’ils forment, ensemble, une relation. Notons que si L1 est inverse de L2, L2 est obligatoirement inverse de L1. Un lien ne peut être inverse que, d’au plus, un autre lien.
Mapper les relations
Une relation (c'est-à-dire un lien libre, ou un couple de liens asservis) est mappée sur une clé étrangère (relation 1-1, 1-N), sur l’égalité des identifiants (relation 1-1) ou à l’aide d’une table intermédiaire (relation 1-N ou N-N). Pour chaque type de relation (sauf N-N), il existe donc plusieurs stratégies qui toutes offrent avantages et inconvénients. Leur étude fine ne sera abordée dans cet article.
Avec Hibernate, le mapping est précisé au niveau des liens : il faut poser une annotation sur chaque lien qui forme une relation. Hibernate (JPA en fait) propose quatre annotations :
- @OneToOne qui indique qu’une entité A (possédant le lien annoté) désigne au plus une entité B et que cette entité B n’est attachée qu’à cette entité A. Cette annotation doit donc être posée sur un lien unique (c'est-à-dire sur un attribut de type référence vers B)
- @OneToMany qui indique qu’une entité A (possédant le lien annoté) désigne une ou plusieurs entités B, mais qu’un B ne peut être attaché qu’à un seul A. Cette annotation doit donc être posée sur un lien multiple (c'est-à-dire sur un attribut collection de références vers B)
- @ManyToOne qui indique qu’une entité A (possédant le lien annoté) désigne au plus une entité B, mais que cette entité B peut être attachée à plusieurs entités A. Cette annotation doit donc être posée sur un lien unique (c'est-à-dire sur un attribut de type référence vers B)
- @ManyToMany qui indique qu’une entité A (possédant le lien annoté) désigne une ou plusieurs entités B, et que ces entités B peuvent être attachées à plusieurs A. Cette annotation doit donc être posée sur un lien multiple (c'est-à-dire sur un attribut collection de référence vers B)
Pour un couple de liens asservis, il faut faire attention à ce que les deux annotations soient compatibles :
- Un lien annoté par @OneToOne ne peut avoir comme inverse qu’un autre lien annoté par @OneToOne.
- Un lien annoté par @OneToMany ne peut avoir comme inverse qu’un lien annoté par @ManyToOne (et vice versa)
- Un lien annoté par @ManyToMany ne peut avoir comme inverse qu’un autre lien annoté par @ManyToMany.
Mais comment indiquer à Hibernate qu’un lien est asservi à un autre et qu’ils forment tous deux une relation ? On le fait en précisant le nom du lien inverse sur l’un d’entre eux à l’aide de l’attribut « mappedBy » des annotations @OneToOne, @OneToMany, @ManyToOne et @ManyToMany. Notez que la valeur de cet attribut est un nom d’attribut et non un nom de classe ! Comme souvent on utilise le nom de la classe désignée comme nom du lien (exemple : attribut « client » de la classe « Contrat » désigne un objet de type « Client »), cela peut prêter à confusion …
On pourrait traduire « mappedBy » par « asservi à ». Dans un couple de lien asservis, un et un seul des deux liens doit avoir son attribut « mappedBy » renseigné. Hibernate utilise l’autre lien comme référence pour le mapping. Les informations complémentaires de mapping (s’il y en a) doivent être portées par le lien qui ne possède pas d’attribut mappedBy sur son annontation).
Petite précision : un lien libre, c'est-à-dire qui n’a pas d’inverse, ne doit pas avoir d’attribut mappedBy renseigné !
Que se passe-t-il si l’attribut « mappedBy » est oublié ? Et bien Hibernate considère que les liens ne sont pas asservis et qu’ils forment chacun une relation indépendante !
Entité propriétaire du lien
L’attribut « mappedBy » est beaucoup plus important qu’il n’y paraît … En effet, lors des mises à jour, Hibernate, pour une relation donnée possède deux liens. Or en face, il n’y a qu’une clé étrangère (ou un enregistrement dans une table de relation pour les relations N-N et certaines relations 1-N). Hibernate ne fait la mise à jour qu’une fois (et non, pour chaque lien, c'est-à-dire deux fois). Hibernate donc « choisit » le lien qui sera utilisé comme source d’information lors de la mise à jour. L’autre lien est ignoré.
Ce lien sélectionné est celui qui ne possède pas l’attribut « mappedBy ». Cet attribut sera donc mis à jour en même temps que l’entité qui le possède. Cette entité est donc « propriétaire » (au sens d’Hibernate) de la relation. Attention ! Cette propriété là est purement technique ! Elle n’a rien à voir avec la notion de composition/agrégation bien connue des concepteurs objets.
Si le code est écrit rigoureusement, c'est-à-dire :
- Si les entités liés A et B sont toutes les deux attachées (i.e. contenues dans le cache de premier niveau de la même session Hibernate),
- Et si les deux liens formant la relation sont mis tous les deux à jour et ceci de façon cohérentes (on indique à A qu’il désigne B et à B qu’il désigne A),
Alors, le choix par Hibernate de l’un ou l’autre lien pour effectuer la mise à jour n’a pas d’importance pour le développeur. Mais – car il y a un « mais », Hibernate ne vérifie rien et part du postulat que le code qu’on lui soumet n’est pas bogué. Donc si une de ces règles n’est pas respectée – ce qui arrive très, très facilement – Hibernate peut, selon les cas, effectuer correctement la mise à jour ou pas. Sans jamais rien dire.
En conséquence, il est impératif de bien vérifier que lorsque l’on met à jour un lien, on met aussi l’inverse à jour. Et il faut s’assurer que les deux entités sont bien attachées à la même session Hibernate. Et surtout, il ne faut pas compter sur Hibernate pour signaler quoi que ce soit en cas d’oubli.
Juste pour donner une idée de la difficulté qu’il peut avoir à mettre à jour correctement un couple de liens, prenons le cas d’un lien de type 1-1 : La classe Voiture est liée à la classe « Moteur » par son attribut « moteur ». Le moteur connait la voiture sur lequel il est installé à l’aide du lien « voiture ».
Très naïvement, nous pourrions écrire :
void affecterMoteur(Voiture v, Moteur m) { v.setMoteur(m); m.setVoiture(v); }
Et bien, ce code est faux ! En effet, il se peut qu’au moment ou cette méthode est appelée, la voiture possède déjà un moteur et que le moteur qu’on veut lui affecter est déjà placé sur une autre voiture. Il faut donc au préalable casser les liens existants. La nouvelle version de notre méthode sera donc :
void affecterMoteur(Voiture v, Moteur m) { if (m.getVoiture() !=null) m.getVoiture().setMoteur(null) ; if (v.getMoteur() !=null) v.getMoteur().setVoiture(null) ; v.setMoteur(m); m.setVoiture(v); }
Okay ? Et bien non ! Ce code est toujours faux ! Car « v » ou « m » peuvent être nul. Le code juste est le suivant :
void affecterMoteur(Voiture v, Moteur m) { if (v.getMoteur()==m) return ; if (m!=null && m.getVoiture() !=null) m.getVoiture().setMoteur(null) ; if (v !=null v.getMoteur() !=null) v.getMoteur().setVoiture(null) ; v.setMoteur(m); m.setVoiture(v); }
Le premier test n’est pas strictement indispensable, mais il permet d’éviter des effets de bord que pourraient induire l’appel inutile aux méthodes « set » (comme celui de positionner un flag « dirty » par exemple).
On peut voir ici, que maintenir la cohérence des liens pour une même relation n’est pas un exercice trivial …
Juste pour vous convaincre, qui n’a pas écrit un constructeur d’entité de ce type ?
public Voiture(Marque marque, String immatriculation, Moteur moteur) { this.marque = marque ; this.immatriculation = immatriculation ; this.moteur = moteur ; }
Et oui … une erreur est si vite arrivée … Surtout lorsqu’on est « aidé » en cela par Eclipse.
Mais alors, pourquoi Hibernate ne se charge pas directement de mettre à jour automatiquement les liens inverses (comme le faisait les EJB 2.0) ? C’est un choix délibéré (de l’équipe Hibernate/JPA, Gavin King en tête). L’idée est de respecter la sémantique de Java. En gros, il s’agit d’apporter un service nouveau (la persistance) à un agrégat d’objets sans en changer la logique classique.
Choisir une entité propriétaire
Comment choisir l’entité propriétaire d’une relation ? Le principe le plus simple à appliquer et le suivant : il faut préférer l’entité mappée à la table qui possède la clé étrangère. En JPA c’est obligatoire. Avec les extensions Hibernate, on peut déroger à cette règle, mais au prix généralement d’un nombre d’accès à la Base de données plus important.
Attention au cas des entités 1-N. L’entité qui possède la clé étrangère est celle qui possède le lien annoté par @ManyToOne. Mais vous avez aussi le droit de décider que l’entité A propriétaire est celle qui possède la collection de B (c'est-à-dire le côté @OneToMany). Comme l’entité A propriétaire et incapable de disposer dans sa propre table d’une clé étrangère qui référence plusieurs T-uple, Hibernate va tenter d’utiliser une table intermédiaire … Et ceci afin de laisser la table des B en complète gestion à la classe B. Vous pouvez forcer Hibernate à utiliser B en ajoutant une annotation @JoinColumn. Mais vous sortez alors de la garantie JPA.
Pour les relations 1-1 et 1-N, donc c’est facile. Mais pour les relations N-N utilisant donc une table intermédiaire, quel est le bon choix ? En fait, il semblerait que choisir l’une ou l’autre des entités soit équivalent dans ce cas. Il existe donc toute une variété d’outil de choix : la pièce de monnaie, le dé (pair/impair), …
Maladie, diagnostic et soin
Tout ce qui vient d’être dit dans cet article doit vous aider à trouver où est la faille dans votre programme lorsque Hibernate ne fait plus correctement son travail. Le symptôme caractéristique d’une erreur dans la gestion des inverses est la mise à jour incomplète :
- Les entités A et B sont modifiées, Les modifications apportées à A comme à B ont été correctement mises à jour, mais la relation reste inchangée. Dans ce cas vous avez probablement oublié de mettre à jour un des deux liens. Et pas de chance, il s’agissait du lien porté par l’entité propriétaire, la seule prise en compte par Hibernate.
- Autre cas de figure qui arrive fréquemment : vous avez bien mis à jour les deux côtés de la relation, mais la relation n’est néanmoins pas mise à jour. Regardez si les autres attributs de l’entité propriétaire ont été mis à jour, eux, si ce n’est pas le cas, c’est que votre entité n’est pas attachée et que vous êtes en train de mélanger entités attachées et détachées, ce qui risque d’être rapidement très fâcheux …
Ce deuxième cas de figure est très traitre. Il arrive fréquemment lorsqu’une pièce de code ajoute une nouvelle entité B à la liste des entités B de A. Hélas, vous avez oublié d’appeler
session.persist(b);
L’entité n’a pas été insérée en Base de données et donc elle est restée en dehors du champ d’Hibernate. Certes, vous l’avez ajoutée à la liste des B de A qui, elle, est attachée. La belle affaire ! C’est B qui est propriétaire du lien. Lorsque A est sauvegardé, la collection des B est purement et simplement – et silencieusement :-( - ignorée par Hibernate, qui aurait parfaitement pu se rendre compte de l’erreur mais qui ne le fait pas !
Il est bien sûr possible d’utiliser la création en cascade (cascade={CascadeType.PERSIST}) à la place d’un appel à persist() pour corriger l’anomalie.



