Hibernate lève MultipleBagFetchException - ne peut pas récupérer simultanément plusieurs sacs

471

Hibernate lève cette exception lors de la création de SessionFactory:

org.hibernate.loader.MultipleBagFetchException: impossible de récupérer simultanément plusieurs sacs

Voici mon cas de test:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 // @IndexColumn(name="INDEX_COL") if I had this the problem solve but I retrieve more children than I have, one child is null.
 private List<Child> children;

}

Child.java

@Entity
public Child {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private Parent parent;

}

Et ce problème? Que puis-je faire?


ÉDITER

OK, le problème que j'ai est qu'une autre entité "parent" est à l'intérieur de mon parent, mon comportement réel est le suivant:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private AnotherParent anotherParent;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<Child> children;

}

AnotherParent.java

@Entity
public AnotherParent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<AnotherChild> anotherChildren;

}

Hibernate n'aime pas deux collections avec FetchType.EAGER, mais cela semble être un bug, je ne fais pas de choses inhabituelles ...

Retrait FetchType.EAGERdu Parentou AnotherParentpermet de résoudre le problème, mais je dois, donc vraie solution est d'utiliser au @LazyCollection(LazyCollectionOption.FALSE)lieu de FetchType(grâce à Bozho pour la solution).

coup
la source
Je demanderais, quelle requête SQL espérez-vous générer qui récupérera deux collections distinctes simultanément? Les types de SQL qui pourraient atteindre ces objectifs nécessiteraient soit une jointure cartésienne (potentiellement très inefficace), soit une UNION de colonnes disjointes (également laides). Vraisemblablement, l'incapacité à réaliser cela en SQL de manière propre et efficace a influencé la conception de l'API.
Thomas W
@ThomasW Voici les requêtes SQL qu'il devrait générer: select * from master; select * from child1 where master_id = :master_id; select * from child2 where master_id = :master_id
nurettin
1
Vous pouvez obtenir une erreur simillar si vous en avez plusieurs List<child>avec fetchTypedéfini pour plus d'un List<clield>
Big Zed

Réponses:

555

Je pense qu'une version plus récente de hibernate (prenant en charge JPA 2.0) devrait gérer cela. Mais sinon, vous pouvez le contourner en annotant les champs de collection avec:

@LazyCollection(LazyCollectionOption.FALSE)

N'oubliez pas de supprimer l' fetchTypeattribut de l' @*ToManyannotation.

Mais notez que dans la plupart des cas, un Set<Child>est plus approprié que List<Child>, donc à moins que vous n'ayez vraiment besoin d'un List- allez-ySet

Mais rappelez-vous qu'avec l'utilisation d'ensembles, vous n'éliminerez pas le produit cartésien sous- jacent tel que décrit par Vlad Mihalcea dans sa réponse !

Bozho
la source
4
bizarre, cela a fonctionné pour moi. Avez-vous retiré le fetchTypedu @*ToMany?
Bozho
102
le problème est que les annotations JPA sont analysées pour ne pas autoriser plus de 2 collectes chargées avec impatience. Mais les annotations spécifiques à l'hibernation le permettent.
Bozho
14
Le besoin de plus d'un EAGER semble totalement réaliste. Cette limitation est-elle simplement un oubli de l'APP? Quelles sont les préoccupations que je dois rechercher lorsque j'ai plusieurs EAGER?
AR3Y35
6
le fait est que hibernate ne peut pas récupérer les deux collections avec une seule requête. Ainsi, lorsque vous interrogez l'entité parent, elle aura besoin de 2 requêtes supplémentaires par résultat, ce qui est normalement quelque chose que vous ne voulez pas.
Bozho
7
Ce serait formidable d'avoir une explication sur la raison pour laquelle cela résout le problème.
Webnet le
290

Changez simplement de Listtype en Settype.

Mais rappelez-vous que vous n'éliminerez pas le produit cartésien sous- jacent tel que décrit par Vlad Mihalcea dans sa réponse !

Ahmad Zyoud
la source
42
Une liste et un ensemble ne sont pas la même chose: un ensemble ne préserve pas l'ordre
Matteo
17
LinkedHashSet conserve l'ordre
egallardo
15
Il s'agit d'une distinction importante et, à bien y penser, tout à fait correcte. Le plusieurs-à-un typique implémenté par une clé étrangère dans la base de données n'est vraiment pas une liste, c'est un ensemble car l'ordre n'est pas préservé. Donc, Set est vraiment plus approprié. Je pense que cela fait la différence en hibernation, même si je ne sais pas pourquoi.
fool4jesus
3
J'avais le même ne peut pas chercher simultanément plusieurs sacs, mais pas à cause des annotations. Dans mon cas, je faisais des jointures gauches et des disjonctions avec les deux *ToMany. Changer le type pour Setrésoudre mon problème aussi. Solution excellente et soignée. Cela devrait être la réponse officielle.
L. Holanda du
20
J'ai aimé la réponse, mais la question à un million de dollars est: pourquoi? Pourquoi avec Set n'affiche pas les exceptions? Merci
Hinotori
140

Ajoutez une annotation @Fetch spécifique à Hibernate à votre code:

@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;

Cela devrait résoudre le problème lié au bogue Hibernate HHH-1718

Dave Richardson
la source
5
@DaveRlz pourquoi subSelect résout ce problème. J'ai essayé votre solution et son fonctionnement, mais vous ne savez pas comment le problème a été résolu en l'utilisant?
HakunaMatata
C'est la meilleure réponse, à moins que cela ait Setvraiment du sens. Avoir une OneToManyrelation unique en utilisant un Setrésultat dans les 1+<# relationships>requêtes, alors qu'en utilisant les FetchMode.SUBSELECTrésultats dans les 1+1requêtes. En outre, l'utilisation de l'annotation dans la réponse acceptée ( LazyCollectionOption.FALSE) entraîne l'exécution de davantage de requêtes.
mstrthealias
1
FetchType.EAGER n'est pas une solution appropriée pour cela. Besoin de poursuivre avec Hibernate Fetch Profiles et besoin de le résoudre
Milinda Bandara
2
Les deux autres meilleures réponses n'ont pas résolu mon problème. Celui-ci l'a fait. Je vous remercie!
Blindworks
3
Est-ce que quelqu'un sait pourquoi SUBSELECT le corrige, mais JOIN ne le fait pas?
Innokenty
42

Cette question a été un thème récurrent à la fois sur StackOverflow ou sur le forum Hibernate, j'ai donc décidé de transformer la réponse en article également.

Considérant que nous avons les entités suivantes:

entrez la description de l'image ici

Et, vous voulez récupérer certaines Postentités parentes avec tous les commentsettags collections .

Si vous utilisez plusieurs JOIN FETCHdirectives:

List<Post> posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "left join fetch p.comments " +
    "left join fetch p.tags " +
    "where p.id between :minId and :maxId", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();

Hibernate jettera l'infâme:

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags [
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.comments,
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.tags
]

Hibernate ne permet pas d'aller chercher plus d'un sac car cela générerait un produit cartésien .

La pire "solution"

Maintenant, vous trouverez de nombreuses réponses, des articles de blog, des vidéos ou d'autres ressources vous indiquant d'utiliser un Setau lieu d'un Listpour vos collections.

C'est un conseil terrible. Ne fais pas ça!

Utiliser Setsau lieu de ListsrendraMultipleBagFetchException disparaître, mais le produit cartésien sera toujours là, ce qui est en fait encore pire, car vous découvrirez le problème de performances longtemps après avoir appliqué ce "correctif".

La bonne solution

Vous pouvez faire l'astuce suivante:

List<Post> posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.comments " +
    "where p.id between :minId and :maxId ", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.tags t " +
    "where p in :posts ", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

Dans la première requête JPQL, distinctNE PAS aller à l'instruction SQL. C'est pourquoi nous avons défini l' PASS_DISTINCT_THROUGHindicateur de requête JPA sur false.

DISTINCT a deux significations dans JPQL, et ici, nous en avons besoin pour dédupliquer les références d'objet Java renvoyées par getResultListdu côté Java, pas du côté SQL. Consultez cet article pour plus de détails.

Tant que vous récupérez au plus une collection en utilisant JOIN FETCH, tout ira bien.

En utilisant plusieurs requêtes, vous éviterez le produit cartésien puisque toute autre collection mais la première est récupérée à l'aide d'une requête secondaire.

Vous pouvez faire plus

Si vous utilisez la FetchType.EAGERstratégie au moment de la cartographie des associations @OneToManyou @ManyToMany, vous pourriez facilement vous retrouver avec unMultipleBagFetchException .

Il vaut mieux passer de FetchType.EAGERà Fetchype.LAZYcar la récupération rapide est une idée terrible qui peut entraîner des problèmes critiques de performances des applications .

Conclusion

Évitez FetchType.EAGERet ne passez pas de Listà Setsimplement parce que cela fera qu'Hibernate cachera le MultipleBagFetchExceptiondessous du tapis. Récupérez une seule collection à la fois et tout ira bien.

Tant que vous le faites avec le même nombre de requêtes que vous avez de collections à initialiser, tout va bien. N'initialisez simplement pas les collections en boucle, car cela déclencherait des problèmes de requête N + 1 , qui sont également mauvais pour les performances.

Vlad Mihalcea
la source
Merci pour la connaissance partagée. Cependant, les DISTINCTperformances sont tueuses dans cette solution. Existe-t-il un moyen de s'en débarrasser distinct? (a essayé de revenir à la Set<...>place, n'a pas beaucoup aidé)
Leonid Dashko
1
DISTINCT ne passe pas à l'instruction SQL. Voilà pourquoi PASS_DISTINCT_THROUGHest réglé sur false. DISTINCT a 2 significations dans JPQL, et ici, nous en avons besoin pour dédupliquer du côté Java, pas du côté SQL. Consultez cet article pour plus de détails.
Vlad Mihalcea
Vlad, merci pour l'aide que je trouve vraiment utile. Cependant, le problème était lié à hibernate.jdbc.fetch_size(finalement je l'ai mis à 350). Par hasard, savez-vous comment optimiser les relations imbriquées? Par exemple, entité1 -> entité2 -> entité3.1, entité 3.2 (où entité3.1 / 3.2 sont des relations @OneToMany)
Leonid Dashko
1
@LeonidDashko Consultez le chapitre Récupération de mon livre Persistance Java haute performance pour de nombreux conseils liés à la récupération des données.
Vlad Mihalcea
1
Non vous ne pouvez pas. Pensez-y en termes de SQL. Vous ne pouvez pas JOINDRE plusieurs associations un-à-plusieurs sans générer un produit cartésien.
Vlad Mihalcea
31

Après avoir essayé avec chaque option décrite dans ces articles et d'autres, je suis arrivé à la conclusion que le correctif est un suivi.

Dans chaque lieu XToMany @ XXXToMany(mappedBy="parent", fetch=FetchType.EAGER) et immédiatement après

@Fetch(value = FetchMode.SUBSELECT)

Cela a fonctionné pour moi

Javier
la source
5
l'ajout @Fetch(value = FetchMode.SUBSELECT)était suffisant
user2601995
1
Il s'agit d'une solution Hibernate uniquement. Que faire si vous utilisez une bibliothèque JPA partagée?
Michel
3
Je suis sûr que vous ne le vouliez pas, mais DaveRlz a déjà écrit la même chose 3 ans plus tôt
phil294
21

Pour le corriger, Setremplacez simplement Listvotre objet imbriqué.

@OneToMany
Set<Your_object> objectList;

et n'oubliez pas d'utiliser fetch=FetchType.EAGER

ça va marcher.

Il y a encore un concept CollectionId dans Hibernate si vous souhaitez vous en tenir à la liste uniquement.

Mais rappelez-vous que vous n'éliminerez pas le produit cartésien sous- jacent tel que décrit par Vlad Mihalcea dans sa réponse !

Prateek Singh
la source
6

vous pouvez conserver les listes EAGER du stand dans JPA et ajouter à au moins l'une d'entre elles l'annotation JPA @OrderColumn (avec évidemment le nom d'un champ à commander). Pas besoin d'annotations d'hibernation spécifiques. Mais gardez à l'esprit qu'il pourrait créer des éléments vides dans la liste si le champ choisi n'a pas de valeurs à partir de 0

 [...]
 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 @OrderColumn(name="orderIndex")
 private List<Child> children;
 [...]

dans Enfants, vous devez ajouter le champ orderIndex

Massimo
la source
2

Nous avons essayé Set au lieu de List et c'est un cauchemar: lorsque vous ajoutez deux nouveaux objets, equals () et hashCode () ne parviennent pas à les distinguer tous les deux! Parce qu'ils n'ont pas d'identifiant.

des outils typiques comme Eclipse génèrent ce type de code à partir des tables de base de données:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
}

Vous pouvez également lire cet article qui explique correctement à quel point JPA / Hibernate est en désordre. Après avoir lu ceci, je pense que c'est la dernière fois que j'utilise un ORM de ma vie.

J'ai également rencontré des gars de conception pilotée par domaine qui disent essentiellement que l'ORM est une chose terrible.

Mike Dudley
la source
1

Lorsque vous avez des objets trop complexes avec une collection sauvage, cela ne peut pas être une bonne idée de les avoir tous avec EAGER fetchType, mieux utiliser LAZY et quand vous avez vraiment besoin de charger les collections, utilisez: Hibernate.initialize(parent.child)pour récupérer les données.

Felipe Cadena
la source
0

Pour moi, le problème était d'avoir des récupérations EAGER imbriquées .

Une solution consiste à définir les champs imbriqués sur LAZY et à utiliser Hibernate.initialize () pour charger le ou les champs imbriqués:

x = session.get(ClassName.class, id);
Hibernate.initialize(x.getNestedField());
butter_prune
la source
0

À ma fin, cela s'est produit lorsque j'avais plusieurs collections avec FetchType.EAGER, comme ceci:

@ManyToMany(fetch = FetchType.EAGER, targetEntity = className.class)
@JoinColumn(name = "myClass_id")
@JsonView(SerializationView.Summary.class)
private Collection<Model> ModelObjects;

De plus, les collections se rejoignaient sur la même colonne.

Pour résoudre ce problème, j'ai changé l'une des collections en FetchType.LAZY car cela convenait à mon cas d'utilisation.

Bonne chance! ~ J

JAD
la source
0

Commenter les deux Fetchet LazyCollectionparfois aider à exécuter le projet.

@Fetch(FetchMode.JOIN)
@LazyCollection(LazyCollectionOption.FALSE)
prisar
la source
0

Une bonne chose @LazyCollection(LazyCollectionOption.FALSE)est que plusieurs champs avec cette annotation peuvent coexister alors qu'ils FetchType.EAGERne le peuvent pas, même dans les situations où une telle coexistence est légitime.

Par exemple, un Orderpeut avoir une liste OrderGroup(courte) ainsi qu'une liste Promotions(également courte). @LazyCollection(LazyCollectionOption.FALSE)peut être utilisé sur les deux sans provoquer LazyInitializationExceptionni l' un ni l'autre MultipleBagFetchException.

Dans mon cas, @Fetchcela a résolu mon problème, MultipleBacFetchExceptionmais ensuite les causes LazyInitializationException, l' no Sessionerreur infâme .

WesternGun
la source
-5

Vous pouvez utiliser une nouvelle annotation pour résoudre ce problème:

@XXXToXXX(targetEntity = XXXX.class, fetch = FetchType.LAZY)

En fait, la valeur par défaut de fetch est également FetchType.LAZY.

leee41
la source
5
JPA3.0 n'existe pas.
holmis83