Les associations de plusieurs à plusieurs sont l’une des associations les plus couramment utilisées avec JPA et Hibernate. Vous pouvez trouver de nombreux exemples pour eux dans le monde réel, et vous pouvez les mapper avec JPA et Hibernate en tant qu’association unidirectionnelle ou bidirectionnelle dans votre modèle de domaine.
Mais vous savez probablement aussi que ces mappages fournissent plusieurs pièges. Dans cet article, je vais vous montrer 5 bonnes pratiques qui vous aideront à éviter ces pièges et à mettre en œuvre des mappages efficaces. Vous apprendrez:
- Le type de données le plus efficace pour votre association
- Pourquoi vous avez besoin de méthodes utilitaires pour gérer votre association
- Le bon type de récupération pour un mappage efficace
- Quand et comment utiliser la récupération spécifique à la requête
- Le CascadeType que vous devez éviter à tout prix
Je ne vais pas plonger dans les détails d’un mappage de base de plusieurs à plusieurs. Si vous ne savez pas exactement comment créer une telle cartographie, veuillez consulter la section plusieurs à plusieurs de mon guide de cartographie d’association.
- Le type de données le plus efficace pour votre association
- Pourquoi vous avez besoin de méthodes utilitaires pour gérer votre association
- Le bon type de recherche pour un mappage efficace
- Quand et comment utiliser la récupération spécifique à la requête
- Le type de cascade que vous devez éviter à tout prix
- Conclusion
Le type de données le plus efficace pour votre association
La plupart des développeurs ne réfléchissent pas beaucoup au type de données d’une association à plusieurs. Ils choisissent simplement un java.util.List car c’est simple et n’effectue aucune vérification pour éviter les doublons.
C’est OK, si vous implémentez une classe Java de base ou si vous modélisez une association Un-à-Plusieurs / Plusieurs-à-Un. Mais vous ne devriez jamais utiliser une Liste si vous modélisez une association Plusieurs à plusieurs.
@Entitypublic class Book {// DON'T DO THIS!!!@ManyToMany@JoinTable(name = "book_author", joinColumns = { @JoinColumn(name = "fk_book") }, inverseJoinColumns = { @JoinColumn(name = "fk_author") })private List<Author> authors = new ArrayList<Author>();...}
Hibernate gère la suppression des opérations sur des relations Plusieurs à plusieurs qui sont mappées à un java.util.Liste très inefficace.
em = emf.createEntityManager();em.getTransaction().begin();// Get Book entity with 2 Authorsb = em.find(Book.class, 1L);// Remove one of the Authorb.getAuthors().remove(a);em.getTransaction().commit();em.close();
Il supprime d’abord tous les enregistrements de la table d’association avant d’insérer tous les enregistrements restants.
09:54:28,876 DEBUG - update Book set title=?, version=? where id=? and version=?09:54:28,878 DEBUG - delete from book_author where fk_book=?09:54:28,882 DEBUG - insert into book_author (fk_book, fk_author) values (?, ?)
Vous devriez plutôt modéliser une association plusieurs à plusieurs en java.util.Définir.
@Entitypublic class Book {@ManyToMany@JoinTable(name = "book_author", joinColumns = { @JoinColumn(name = "fk_book") }, inverseJoinColumns = { @JoinColumn(name = "fk_author") })private Set<Author> authors = new HashSet<Author>();...}
Hibernate gère alors beaucoup mieux les opérations de suppression sur l’association. Il ne supprime plus que les enregistrements attendus de l’association et conserve les autres intacts.
10:00:37,709 DEBUG - update Book set title=?, version=? where id=? and version=?10:00:37,711 DEBUG - delete from book_author where fk_book=? and fk_author=?
Pourquoi vous avez besoin de méthodes utilitaires pour gérer votre association
Les associations bidirectionnelles sont mappées à un attribut d’entité aux deux extrémités des relations. Ainsi, dans l’exemple précédent, vous avez un attribut authors sur l’entité Book et un attribut book sur l’entité Author. Cela rend l’implémentation d’un JPQL ou d’un CriteriaQuery très confortable car vous pouvez utiliser ces attributs pour définir une clause de JOINTURE.
Mais l’ajout ou la suppression d’une association devient plus compliqué. Vous devez toujours effectuer le changement aux deux extrémités de l’association. Par exemple, si vous souhaitez ajouter un livre à Author, vous devez l’ajouter à l’attribut books de l’entité Author, et vous devez également ajouter l’attribut Author the authors sur l’entité Book. Sinon, votre contexte de persistance actuel contient des données incohérentes que vous utiliserez jusqu’à la fin de votre transaction en cours.
Book b = new Book();b.setTitle("Hibernate Tips - More than 70 solutions to common Hibernate problems");em.persist(b);Author a = em.find(Author.class, 1L);a.getBooks().add(b);b.getAuthors().add(a);
Les méthodes utilitaires sur vos entités Auteur et Livre facilitent la mise à jour et la suppression. Au sein de ces méthodes, vous effectuez les opérations requises sur les deux entités.
@Entitypublic class Author {@ManyToMany(mappedBy = "authors")private Set<Book> books = new HashSet<Book>();...public void addBook(Book book) {this.books.add(book);book.getAuthors().add(this);}public void removeBook(Book book) {this.books.remove(book);book.getAuthors().remove(this);}}
Le bon type de recherche pour un mappage efficace
Ceci est rapide. Vous devez toujours utiliser FetchType.PARESSEUX pour vos nombreuses associations. Il indique à votre fournisseur de persistance de ne pas récupérer les entités associées de la base de données tant que vous ne les utilisez pas. C’est généralement le cas lorsque vous appelez sa méthode getter pour la première fois.
Heureusement, c’est la valeur par défaut pour toutes les associations. Alors, assurez-vous de ne pas le changer.
Et si vous souhaitez en savoir plus sur les différents types de FetchTypes de JPA, jetez un coup d’œil à mon Introduction aux types de FetchTypes de JPA.
Quand et comment utiliser la récupération spécifique à la requête
Si vous utilisez FetchType.PARESSEUX, vous devez connaître la récupération spécifique à la requête. Sinon, votre application sera très lente car vous avez créé beaucoup de n + 1 problèmes de sélection.
Lorsque vous chargez une entité et utilisez une récupération spécifique à la requête, vous indiquez à Hibernate les associations mappées qu’elle doit initialiser pour chaque entité récupérée. Il étend ensuite la clause SELECT de votre requête afin qu’elle inclue les colonnes mappées par ces autres entités et initialise les associations. Et comme les associations sont déjà initialisées, Hibernate n’a pas besoin d’effectuer une requête supplémentaire lorsque vous accédez à sa méthode getter pour la première fois.
Vous pouvez implémenter l’extraction spécifique à la requête de plusieurs manières différentes. La plus simple est une clause de récupération de JOINTURE, que je vais vous montrer ici. Mais vous pouvez également utiliser un @NamedEntityGraph ou un EntityGraph, ce que j’ai expliqué dans des articles précédents.
La définition d’une clause de récupération de JOINTURE est presque identique à une clause de JOINTURE simple dans une requête JPQL. Il vous suffit d’ajouter le mot clé FETCH.
Author a = em.createQuery("SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = 1", Author.class).getSingleResult();
Même ainsi, une clause JOIN et une clause JOIN FETCH semblent très similaires, la clause JOIN FETCH a un effet beaucoup plus important sur la requête SQL générée. Il est non seulement traduit en une JOINTURE SQL, comme c’est le cas pour une clause de jointure JPQL, mais il oblige également votre fournisseur de persistance à étendre la clause SELECT par toutes les colonnes mappées par l’entité associée.
16:21:03,046 DEBUG SQL:94 - select author0_.id as id1_0_0_, book2_.id as id1_1_1_, author0_.firstName as firstNam2_0_0_, author0_.lastName as lastName3_0_0_, author0_.version as version4_0_0_, book2_.format as format2_1_1_, book2_.publishingDate as publishi3_1_1_, book2_.title as title4_1_1_, book2_.version as version5_1_1_, books1_.author_id as author_i2_2_0__, books1_.book_id as book_id1_2_0__ from Author author0_ inner join book_author books1_ on author0_.id=books1_.author_id inner join Book book2_ on books1_.book_id=book2_.id where author0_.id=1
Le type de cascade que vous devez éviter à tout prix
Si vous activez la cascade sur une association, votre fournisseur de persistance applique les opérations que vous effectuez sur l’entité à toutes les entités associées. S’il le fait pour toutes les opérations ou juste pour quelques opérations sélectionnées dépend du CascadeType configuré.
Cela peut sembler une idée étonnante qui facilite beaucoup la mise en œuvre de votre logique métier. Et ce n’est pas tout à fait faux.
Mais évitez les types de cascades REMOVE et ALL, qui incluent REMOVE, pour plusieurs associations. Dans le meilleur des cas, cela ne crée que des problèmes de performances, mais dans le pire des cas, il peut également supprimer plus d’enregistrements que prévu.
J’ai expliqué les deux pièges et leur solution en détail dans un article précédent. Ou si vous voulez rester simple, déclenchez les informations requises par programme sur les entités associées. Cela peut nécessiter quelques lignes de code supplémentaires, mais cela évite tout effet secondaire inattendu.
Conclusion
Vous pouvez trouver beaucoup d’exemples pour de nombreuses associations dans le monde réel, et vous pouvez facilement les mapper avec JPA et Hibernate. Malheureusement, ces mappages simples cachent quelques pièges que vous pouvez éviter en suivant ces 5 bonnes pratiques:
- Associations de modèles en java.util.Définir.
- Fournit des méthodes utilitaires pour ajouter ou supprimer une entité d’une association.
- Utilisez toujours FetchType.LAZY, qui est la valeur par défaut, pour éviter les problèmes de performances.
- Appliquez une récupération spécifique à la requête pour éviter les problèmes de sélection n+1.
- N’utilisez pas les types de cascade REMOVE et ALL.