Vous avez probablement appris que vous devriez utiliser FetchType.PARESSEUX pour toutes vos associations. Il garantit que Hibernate initialise une association lorsque vous l’utilisez et ne passe pas de temps à obtenir des données dont vous n’avez pas besoin.
Malheureusement, cela introduit un nouveau problème. Vous devez maintenant utiliser une clause JOIN FETCH ou un EntityGraph pour récupérer l’association si vous en avez besoin. Sinon, vous rencontrerez le problème de sélection n + 1, qui provoque de graves problèmes de performances ou une exception Lazyinitialization. Si vous faites cela pour plusieurs associations, Hibernate peut lancer une exception MultipleBagFetchException.
Dans cet article, je vais vous expliquer quand Hibernate lève cette exception et vous montrer vos 2 meilleures options pour y remédier. L’un d’eux convient parfaitement aux associations avec une petite cardinalité et l’autre aux associations contenant beaucoup d’éléments. Alors, jetons un coup d’œil aux deux, et vous choisissez celui qui correspond à votre application.
Cause de l’exception MultipleBagFetchException
Comme je l’ai expliqué dans un article précédent sur le type de données le plus efficace pour une association à plusieurs, la dénomination interne d’Hibernate des types de collection est assez déroutante. Hibernate l’appelle un sac, si les éléments de votre java.util.La liste n’est pas ordonnée. S’ils sont commandés, cela s’appelle une liste.
Donc, en fonction de votre mappage, un java.util.La liste peut être traitée comme un sac ou une liste. Mais ne vous inquiétez pas, dans la vraie vie, ce n’est pas aussi déroutant que cela puisse paraître. La définition de l’ordre d’une association nécessite une annotation supplémentaire et représente presque toujours une surcharge. C’est pourquoi vous devriez l’éviter et pourquoi au moins 90% des mappages d’association qui utilisent un java.util.Liste et que j’ai vu dans de vrais projets ne sont pas ordonnés. Donc, Hibernate les traite comme un sac.
Voici un modèle de domaine simple dans lequel Hibernate traite les Critiques et les Auteurs d’un Livre comme des Sacs.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
Si vous essayez de récupérer plusieurs de ces sacs dans une requête JPQL, vous créez un produit cartésien.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.authors a "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> b = q.getResultList();
Cela peut créer des problèmes de performances. Hibernate a également du mal à différencier les informations censées être dupliquées des informations dupliquées à cause du produit cartésien. Pour cette raison, Hibernate lance une exception MultipleBagFetchException.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
Correction de l’exception MultipleBagFetchException
Vous pouvez trouver beaucoup de questions sur cette exception et diverses solutions pour l’éviter. Mais beaucoup d’entre eux ont des effets secondaires inattendus. Les seuls correctifs 2 entre lesquels vous devez choisir sont ceux que je décrirai dans les sections suivantes. Laquelle d’entre elles est la meilleure pour vous dépend de la taille du produit cartésien que vos requêtes peuvent créer :
- Si toutes vos associations ne contiennent qu’un petit nombre d’éléments, le produit cartésien créé sera relativement petit. Dans ces situations, vous pouvez modifier les types d’attributs qui mappent vos associations à un java.util.Définir. Hibernate peut ensuite récupérer plusieurs associations dans 1 requête.
- Si au moins une de vos associations contient beaucoup d’éléments, votre produit cartésien deviendra trop gros pour le récupérer efficacement dans 1 requête. Vous devez ensuite utiliser plusieurs requêtes qui obtiennent différentes parties du résultat requis.
Comme toujours, l’optimisation des performances de votre application nécessite de choisir entre différents compromis, et il n’y a pas d’approche unique. Les performances de chaque option dépendent de la taille du produit cartésien et du nombre de requêtes que vous exécutez. Pour un produit cartésien relativement petit, obtenir toutes les informations avec 1 requête vous offre les meilleures performances. Si le produit cartésien atteint une certaine taille, vous devriez mieux le diviser en plusieurs requêtes.
C’est pourquoi je vais vous montrer les deux options afin que vous puissiez choisir celle qui convient à votre application.
Option 1: Utilisez un ensemble au lieu d’une liste
L’approche la plus simple pour corriger l’exception MultipleBagFetchException consiste à changer le type des attributs qui mapper vos associations à plusieurs vers un java.util.Définir. Il ne s’agit que d’un petit changement dans votre mappage et vous n’avez pas besoin de modifier votre code métier.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
Comme expliqué précédemment, si vous effectuez maintenant la même requête que je vous ai montrée auparavant pour obtenir le Livre avec tous ses Auteurs et critiques, votre jeu de résultats contiendra un produit cartésien. La taille de ce produit dépend du nombre de livres que vous sélectionnez et du nombre d’auteurs et de critiques associés.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.authors a "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> b = q.getResultList();
Ici vous pouvez voir la requête SQL générée. Pour obtenir toutes les associations demandées, Hibernate doit sélectionner toutes les colonnes mappées par ces entités. En combinaison avec le produit cartésien créé par les 3 jointures INTERNES, cela peut devenir un problème de performance.
19:46:20,785 DEBUG - select book0_.id as id1_1_0_, author2_.id as id1_0_1_, reviews3_.id as id1_4_2_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, author2_.firstName as firstNam2_0_1_, author2_.lastName as lastName3_0_1_, author2_.version as version4_0_1_, authors1_.bookId as bookId1_2_0__, authors1_.authorId as authorId2_2_0__, reviews3_.bookid as bookid3_4_2_, reviews3_.comment as comment2_4_2_, reviews3_.bookid as bookid3_4_1__, reviews3_.id as id1_4_1__ from Book book0_ inner join BookAuthor authors1_ on book0_.id=authors1_.bookId inner join Author author2_ on authors1_.authorId=author2_.id inner join Review reviews3_ on book0_.id=reviews3_.bookid where book0_.id=1
Chaque fois que vous écrivez une telle requête, vous devez également garder à l’esprit que Hibernate ne cache pas que le jeu de résultats contient un produit. Cette requête renvoie chaque livre plusieurs fois. Le nombre de références à un même objet de livre est calculé par le nombre d’auteurs multiplié par le nombre de critiques. Vous pouvez éviter cela en ajoutant le mot clé DISTINCT à votre clause select et en définissant l’indice de requête hibernate.requête.passDistinctThrough à false.
Considérations relatives aux performances
Dans cet exemple, ma requête ne sélectionne que 1 livre et la plupart des livres ont été écrits par 1 à 3 auteurs. Ainsi, même si la base de données contient plusieurs critiques pour ce livre, le produit cartésien sera toujours relativement petit.
Sur la base de ces hypothèses, il pourrait être plus rapide d’accepter l’inefficacité du produit cartésien pour réduire le nombre de requêtes. Cela peut changer si votre produit cartésien devient plus grand parce que vous sélectionnez un grand nombre de Livres ou si votre Livre moyen a été écrit par quelques dizaines d’auteurs.
Option 2: La diviser en plusieurs requêtes
Récupérer d’énormes produits cartésiens dans 1 requête est inefficace. Cela nécessite beaucoup de ressources dans votre base de données et met une charge inutile sur votre réseau. Hibernate et votre pilote JDBC doivent également dépenser plus de ressources pour gérer le résultat de la requête.
Vous pouvez éviter cela en effectuant plusieurs requêtes qui récupèrent différentes parties du graphique d’entités requis. Dans l’exemple de ce post, je récupérerais les Livres avec tous leurs Auteurs dans 1 requête et les Livres avec toutes leurs Critiques dans une 2ème requête. Si votre graphique d’entités requises est plus complexe, vous devrez peut-être utiliser plus de requêtes ou récupérer plus d’associations avec chacune d’elles.
TypedQuery<Book> q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b JOIN FETCH b.authors a "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);List<Book> books = q.getResultList();log.info(books.get(0));q = em.createQuery("SELECT DISTINCT b "+ "FROM Book b "+ "JOIN FETCH b.reviews r "+ "WHERE b.id = 1",Book.class);q.setHint(QueryHints.PASS_DISTINCT_THROUGH, false);books = q.getResultList();log.info(books.get(0));log.info("Authors: "+books.get(0).getAuthors().size());log.info("Reviews: "+books.get(0).getReviews().size());
Comme je l’ai expliqué dans le post de la semaine dernière, Hibernate garantit qu’au sein de chaque session, il n’y a qu’un seul objet entité qui représente un enregistrement spécifique dans la base de données. Vous pouvez l’utiliser pour résoudre efficacement les références de clés étrangères ou pour permettre à Hibernate de fusionner les résultats de plusieurs requêtes.
Si vous regardez la sortie du journal suivante, vous pouvez voir que les listes renvoyées par les deux requêtes contiennent exactement le même objet. Dans les deux cas, les objets de livre ont la référence @1f.
Lorsque Hibernate a traité le résultat de la 2ème requête, il a vérifié pour chaque enregistrement si le cache de 1er niveau contenait déjà un objet pour cette entité de livre. Il a ensuite réutilisé cet objet et ajouté l’examen renvoyé à l’association mappée.
19:52:10,600 DEBUG - select book0_.id as id1_1_0_, author2_.id as id1_0_1_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, author2_.firstName as firstNam2_0_1_, author2_.lastName as lastName3_0_1_, author2_.version as version4_0_1_, authors1_.bookId as bookId1_2_0__, authors1_.authorId as authorId2_2_0__ from Book book0_ inner join BookAuthor authors1_ on book0_.id=authors1_.bookId inner join Author author2_ on authors1_.authorId=author2_.id where book0_.id=119:52:10,633 INFO - 19:52:10,645 DEBUG - select book0_.id as id1_1_0_, reviews1_.id as id1_4_1_, book0_.publisherid as publishe5_1_0_, book0_.publishingDate as publishi2_1_0_, book0_.title as title3_1_0_, book0_.version as version4_1_0_, reviews1_.bookid as bookid3_4_1_, reviews1_.comment as comment2_4_1_, reviews1_.bookid as bookid3_4_0__, reviews1_.id as id1_4_0__ from Book book0_ inner join Review reviews1_ on book0_.id=reviews1_.bookid where book0_.id=119:52:10,648 INFO - 19:52:10,648 INFO - Authors: 219:52:10,648 INFO - Reviews: 2
Considérations de performance
Si vous utilisez plusieurs requêtes pour obtenir le graphique requis des entités, vous évitez la création d’un énorme produit cartésien. Cela réduit la charge sur tous les systèmes impliqués et facilite la garantie de bonnes performances pour toutes les requêtes.
Mais cela ne signifie pas nécessairement que cette approche est plus rapide que l’option 1. Vous effectuez maintenant plus de requêtes qu’auparavant. Chacun d’eux nécessite un aller-retour dans la base de données et crée une surcharge de gestion dans la base de données, par exemple pour créer un plan d’exécution. De ce fait, cette option n’est plus rapide que l’option 1, si la taille du produit cartésien crée une surcharge plus importante que l’exécution de plusieurs requêtes.
Conclusion
Comme vous l’avez vu dans cet article, vous pouvez résoudre l’exception MultipleBagFetchException d’Hibernate de 2 manières:
- Vous pouvez modifier le type de données de l’attribut qui mappe les associations et récupérer toutes les informations dans 1 requête. Le résultat de cette requête est un produit cartésien. Tant que ce produit ne devient pas trop gros, cette approche est simple et efficace.
- Vous pouvez utiliser plusieurs requêtes pour récupérer le graphique d’entités requis. Cela évite un énorme produit cartésien et constitue la meilleure approche si vous devez récupérer une énorme quantité de données.