Hibernacja zgłasza wyjątek MultipleBagFetchException - nie można jednocześnie pobrać wielu toreb

471

Hibernacja zgłasza ten wyjątek podczas tworzenia SessionFactory:

org.hibernate.loader.MultipleBagFetchException: nie można jednocześnie pobrać wielu torebek

To mój przypadek testowy:

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;

}

Co powiesz na ten problem? Co mogę zrobić?


EDYTOWAĆ

OK, mam problem z tym, że inny „rodzic” jest wewnątrz mojego rodzica, moje prawdziwe zachowanie jest takie:

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;

}

Hibernacja nie lubi dwóch kolekcji FetchType.EAGER, ale wydaje się, że to błąd, nie robię niezwykłych rzeczy ...

Usuwanie FetchType.EAGERz Parentlub AnotherParentrozwiązuje problem, ale muszę, więc prawdziwym rozwiązaniem jest użycie @LazyCollection(LazyCollectionOption.FALSE)zamiast FetchType(dzięki Bozho do roztworu).

cios
źródło
Chciałbym zapytać, jakie zapytanie SQL chcesz wygenerować, aby pobrać jednocześnie dwie osobne kolekcje? Rodzaje języka SQL, które byłyby w stanie to osiągnąć, wymagałyby połączenia kartezjańskiego (potencjalnie wysoce nieefektywnego) lub UNII rozłącznych kolumn (również brzydkich). Prawdopodobnie niemożność osiągnięcia tego w SQL w czysty i wydajny sposób wpłynęła na projekt API.
Thomas W
@ThomasW Są to zapytania SQL, które powinien wygenerować:select * from master; select * from child1 where master_id = :master_id; select * from child2 where master_id = :master_id
nurettin
1
Możesz otrzymać błąd symulacyjny, jeśli masz więcej niż jeden List<child>ze fetchTypezdefiniowanym dla więcej niż jednego List<clield>
Big Zed

Odpowiedzi:

555

Myślę, że nowsza wersja hibernacji (obsługująca JPA 2.0) powinna sobie z tym poradzić. Ale w przeciwnym razie możesz to obejść, dodając adnotacje do pól kolekcji:

@LazyCollection(LazyCollectionOption.FALSE)

Pamiętaj, aby usunąć fetchTypeatrybut z @*ToManyadnotacji.

Ale zauważ, że w większości przypadków a Set<Child>jest bardziej odpowiednie niż List<Child>, więc chyba, że ​​naprawdę potrzebujesz List- idź doSet

Przypomnij jednak, że używając zestawów nie wyeliminujesz podkładającego się produktu kartezjańskiego, jak opisał Vlad Mihalcea w swojej odpowiedzi !

Bozho
źródło
4
dziwne, to zadziałało dla mnie. Czy usunąłeś fetchTypez @*ToMany?
Bozho,
101
problem polega na tym, że adnotacje JPA są analizowane, aby nie pozwolić na więcej niż 2 niecierpliwie ładowanych kolekcji. Ale pozwalają na to adnotacje dotyczące hibernacji.
Bozho
14
Potrzeba więcej niż 1 EAGER wydaje się całkowicie realistyczna. Czy to ograniczenie jest tylko nadzorem WZP? Jakie są obawy, na które powinienem zwrócić uwagę, mając wiele EAGERÓW?
AR3Y35,
6
Chodzi o to, że hibernacja nie może pobrać dwóch kolekcji za pomocą jednego zapytania. Kiedy więc zapytasz o jednostkę nadrzędną, będzie ona potrzebować 2 dodatkowych zapytań na wynik, co zwykle jest czymś, czego nie chcesz.
Bozho
7
Byłoby wspaniale mieć wyjaśnienie, dlaczego to rozwiązuje problem.
Webnet
290

Wystarczy zmienić Listtyp na Settyp.

Przypomnij jednak, że nie wyeliminujesz podkładającego się produktu kartezjańskiego, jak opisał Vlad Mihalcea w swojej odpowiedzi !

Ahmad Zyoud
źródło
42
Lista i zestaw to nie to samo: zestaw nie zachowuje porządku
Matteo
17
LinkedHashSet zachowuje porządek
egallardo
15
To ważne rozróżnienie i, jeśli się nad tym zastanowić, całkowicie poprawne. Typowa funkcja wiele do jednego zaimplementowana przez klucz obcy w DB tak naprawdę nie jest Listą, to Zestaw, ponieważ kolejność nie jest zachowana. Więc Set jest naprawdę bardziej odpowiedni. Myślę, że to robi różnicę w hibernacji, chociaż nie wiem dlaczego.
fool4jesus
3
Miałem to samo, że nie mogę jednocześnie pobrać wielu torebek, ale nie z powodu adnotacji. W moim przypadku robiłem lewe połączenia i rozłączenia z tymi dwoma *ToMany. Zmieniłem też typ, aby Setrozwiązać mój problem. Doskonałe i schludne rozwiązanie. To powinna być oficjalna odpowiedź.
L. Holanda,
20
Podobała mi się odpowiedź, ale pytanie za milion dolarów brzmi: dlaczego? Dlaczego w programie Set nie wyświetla się wyjątków? Dzięki
Hinotori,
140

Dodaj adnotację @Fetch do swojego kodu:

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

To powinno rozwiązać problem związany z błędem Hibernacji HHH-1718

Dave Richardson
źródło
5
@DaveRlz dlaczego subSelect rozwiązuje ten problem. Wypróbowałem Twoje rozwiązanie i jego działanie, ale nie wiesz, jak rozwiązać ten problem?
HakunaMatata,
To najlepsza odpowiedź, chyba że Setnaprawdę ma sens. Posiadanie pojedynczej OneToManyrelacji przy użyciu Setwyników w 1+<# relationships>zapytaniach, gdzie jak przy użyciu FetchMode.SUBSELECTwyników w 1+1zapytaniach. Ponadto użycie adnotacji w zaakceptowanej odpowiedzi ( LazyCollectionOption.FALSE) powoduje wykonanie jeszcze większej liczby zapytań.
mstrthealias
1
FetchType.EAGER nie jest do tego właściwym rozwiązaniem. Musisz przejść do Hibernacji i pobrać profile
Milinda Bandara
2
Dwie pozostałe najważniejsze odpowiedzi nie rozwiązały mojego problemu. Ten zrobił. Dziękuję Ci!
Blindworks
3
Czy ktoś wie, dlaczego SUBSELECT to naprawia, ale JOIN nie?
Innokenty,
41

To pytanie było powtarzającym się motywem zarówno na StackOverflow, jak i na forum Hibernacji, więc postanowiłem zamienić odpowiedź również w artykuł .

Biorąc pod uwagę, że mamy następujące podmioty:

wprowadź opis zdjęcia tutaj

I chcesz pobrać niektóre Postelementy nadrzędne wraz ze wszystkimi zbiorami commentsi tags.

Jeśli używasz więcej niż jednej JOIN FETCHdyrektywy:

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();

Hibernacja rzuci niesławny:

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
]

Hibernacja nie pozwala na pobranie więcej niż jednej torby, ponieważ wygenerowałoby to produkt kartezjański .

Najgorsze „rozwiązanie”

Teraz znajdziesz wiele odpowiedzi, postów na blogu, filmów lub innych zasobów, w których możesz użyć Setzamiast Listswoich kolekcji.

To okropna rada. Nie rób tego!

Użycie Setszamiast Listsspowoduje, że MultipleBagFetchExceptionzniknie, ale produkt kartezjański nadal tam będzie, co jest nawet gorsze, ponieważ problem z wydajnością dowiesz się długo po zastosowaniu tej „poprawki”.

Właściwe rozwiązanie

Możesz wykonać następującą sztuczkę:

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();

W pierwszym zapytaniu JPQL distinctNIE przechodzi do instrukcji SQL. Dlatego ustawiliśmy PASS_DISTINCT_THROUGHwskazówkę dotyczącą zapytania JPA na false.

DISTINCT ma dwa znaczenia w języku JPQL i tutaj potrzebujemy go do deduplikacji odwołań do obiektów Java zwróconych getResultListpo stronie Java, a nie po stronie SQL. Sprawdź ten artykuł, aby uzyskać więcej informacji.

Tak długo, jak JOIN FETCHpobierzesz co najwyżej jedną kolekcję , nic ci nie będzie.

Używając wielu zapytań, unikniesz produktu kartezjańskiego, ponieważ jakakolwiek inna kolekcja jest pobierana przy użyciu zapytania wtórnego.

Możesz zrobić więcej

Jeśli używasz FetchType.EAGERstrategii w czasie mapowania dla stowarzyszeń @OneToManylub @ManyToManystowarzyszeń, możesz łatwo skończyć z MultipleBagFetchException.

Lepiej jest przełączać się z FetchType.EAGERna, Fetchype.LAZYponieważ chętne pobieranie jest okropnym pomysłem, który może prowadzić do krytycznych problemów z wydajnością aplikacji .

Wniosek

Unikaj FetchType.EAGERi nie zmieniaj opcji z Listna Settylko dlatego, że spowoduje to, że Hibernacja ukryje MultipleBagFetchExceptionpod dywan. Pobieraj tylko jedną kolekcję na raz, a wszystko będzie dobrze.

Tak długo, jak robisz to z taką samą liczbą zapytań, jak masz kolekcje do zainicjowania, nic ci nie jest. Po prostu nie inicjuj kolekcji w pętli, ponieważ spowoduje to problemy z zapytaniami N + 1 , które również mają negatywny wpływ na wydajność.

Vlad Mihalcea
źródło
Dzięki za wspólną wiedzę. Jednak DISTINCTw tym rozwiązaniu zabija wydajność. Czy jest sposób na pozbycie się distinct? (próbowała wrócić Set<...>zamiast, nie pomogło)
Leonid Dashko
1
DISTINCT nie przechodzi do instrukcji SQL. Dlatego PASS_DISTINCT_THROUGHjest ustawiony na false. DISTINCT ma 2 znaczenia w języku JPQL, a tutaj potrzebujemy go do deduplikacji po stronie Java, a nie po stronie SQL. Sprawdź ten artykuł, aby uzyskać więcej informacji.
Vlad Mihalcea,
Vlad, dzięki za pomoc, którą uważam za naprawdę przydatną. Problem był jednak związany hibernate.jdbc.fetch_size(ostatecznie ustawiłem go na 350). Czy wiesz, jak zoptymalizować relacje zagnieżdżone? Np. Byt1 -> byt2 -> byt3.1, byt 3.2 (gdzie byt3.1 / 3.2 są relacjami @OneToMany)
Leonid Dashko
1
@LeonidDashko Zapoznaj się z rozdziałem Pobieranie w mojej książce o wysokiej wydajności Java Persistence, aby uzyskać wiele wskazówek związanych z pobieraniem danych.
Vlad Mihalcea
1
Nie, nie możesz. Pomyśl o tym w kategoriach SQL. Nie można ŁĄCZYĆ wielu powiązań jeden do wielu bez generowania produktu kartezjańskiego.
Vlad Mihalcea
31

Po wypróbowaniu każdej opcji opisanej w tych postach i innych, doszedłem do wniosku, że poprawka jest następująca.

W każdym miejscu XToMany @ XXXToMany(mappedBy="parent", fetch=FetchType.EAGER) i bezpośrednio po nim

@Fetch(value = FetchMode.SUBSELECT)

To zadziałało dla mnie

Javier
źródło
5
dodanie @Fetch(value = FetchMode.SUBSELECT)było wystarczające
user2601995
1
Jest to rozwiązanie tylko hibernacyjne. Co się stanie, jeśli korzystasz ze wspólnej biblioteki JPA?
Michel
3
Jestem pewien, że nie chciałeś, ale DaveRlz napisał już to samo 3 lata wcześniej
phil294,
21

Aby rozwiązać to po prostu wziąć Setw miejscu Listdla zagnieżdżonego obiektu.

@OneToMany
Set<Your_object> objectList;

i nie zapomnij użyć fetch=FetchType.EAGER

to będzie działać.

Jest jeszcze jedna koncepcja CollectionIdw Hibernacji, jeśli chcesz trzymać się tylko listy.

Przypomnij jednak, że nie wyeliminujesz podkładającego się produktu kartezjańskiego, jak opisał Vlad Mihalcea w swojej odpowiedzi !

Prateek Singh
źródło
6

możesz przechowywać listy EAGER na stoisku w JPA i dodać do co najmniej jednej z nich JPA adnotacja @OrderColumn (oczywiście nazwa pola do zamówienia). Nie potrzeba specjalnych adnotacji hibernacji. Pamiętaj jednak, że może tworzyć puste elementy na liście, jeśli wybrane pole nie ma wartości zaczynających się od 0

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

w Dzieci należy dodać pole orderIndex

Massimo
źródło
2

Wypróbowaliśmy Set zamiast List i jest to koszmar: kiedy dodajesz dwa nowe obiekty, równa się () i hashCode () nie rozpoznają obu! Ponieważ nie mają żadnego identyfikatora.

typowe narzędzia, takie jak Eclipse, generują ten rodzaj kodu z tabel bazy danych:

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

Możesz również przeczytać ten artykuł który prawidłowo wyjaśnia, jak popsuty jest JPA / Hibernacja. Po przeczytaniu tego, myślę, że po raz ostatni używam ORM w moim życiu.

Spotkałem też facetów zajmujących się projektowaniem opartym na domenie, którzy twierdzą, że ORM to straszna rzecz.

Mike Dudley
źródło
1

Gdy masz zbyt skomplikowane obiekty z kolekcją oszczędzającą, nie byłoby dobrym pomysłem mieć je wszystkie z EAGER fetchType, lepiej użyj LAZY, a gdy naprawdę potrzebujesz załadować kolekcje, użyj: Hibernate.initialize(parent.child)aby pobrać dane.

Felipe Cadena
źródło
0

Dla mnie problemem było zagnieżdżenie pobrań EAGER .

Jednym z rozwiązań jest ustawienie zagnieżdżonych pól na LAZY i użycie Hibernate.initialize () do załadowania zagnieżdżonych pól:

x = session.get(ClassName.class, id);
Hibernate.initialize(x.getNestedField());
masło śliwkowe
źródło
0

Na moim końcu stało się tak, gdy miałem wiele kolekcji z FetchType.EAGER, takich jak:

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

Ponadto kolekcje łączyły się w tej samej kolumnie.

Aby rozwiązać ten problem, zmieniłem jedną z kolekcji na FetchType.LAZY, ponieważ w moim przypadku użycia było to w porządku.

Powodzenia! ~ J

JAD
źródło
0

Komentowanie obu, Fetcha LazyCollectionczasem pomaga uruchomić projekt.

@Fetch(FetchMode.JOIN)
@LazyCollection(LazyCollectionOption.FALSE)
prisar
źródło
0

Jedną dobrą rzeczą @LazyCollection(LazyCollectionOption.FALSE)jest to, że kilka pól z tą adnotacją może koegzystować, podczas gdy FetchType.EAGERnie, nawet w sytuacjach, w których takie współistnienie jest uzasadnione.

Na przykład, Ordermoże mieć listę OrderGroup(krótką), a także listę Promotions(również krótką). @LazyCollection(LazyCollectionOption.FALSE)może być stosowany na obu bez powodowania LazyInitializationExceptionżadnego MultipleBagFetchException.

W moim przypadku @Fetchrozwiązałem problem, MultipleBacFetchExceptionale potem powoduje LazyInitializationExceptionniesławny no Sessionbłąd.

WesternGun
źródło
-5

Aby rozwiązać ten problem, możesz użyć nowej adnotacji:

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

W rzeczywistości domyślną wartością fetch jest również FetchType.LAZY.

leee41
źródło
5
JPA3.0 nie istnieje.
holmis83,