Twoje 2 najlepsze opcje, aby naprawić Hibernate MultipleBagFetchException

prawdopodobnie nauczyłeś się, że powinieneś używać FetchType.Leniwy dla wszystkich Twoich skojarzeń. Zapewnia to, że Hibernate inicjalizuje skojarzenie, gdy go używasz i nie spędza czasu na pobieraniu danych, których nie potrzebujesz.

Niestety wprowadza to nowy problem. Musisz teraz użyć klauzuli join FETCH lub Entitygrafu, aby pobrać Stowarzyszenie, jeśli tego potrzebujesz. W przeciwnym razie wystąpi problem N+1 select, który powoduje poważne problemy z wydajnością lub wrażenie LazyInitializationException. Jeśli zrobisz to dla wielu skojarzeń, Hibernate może rzucić MultipleBagFetchException.

w tym artykule wyjaśnię, kiedy Hibernate rzuca ten wyjątek i pokażę Ci 2 najlepsze opcje, aby to naprawić. Jeden z nich świetnie nadaje się do skojarzeń o małej kardynalności, a drugi do skojarzeń zawierających wiele elementów. Przyjrzyjmy się im obu i wybierzemy ten, który pasuje do Twojej aplikacji.

przyczyna MultipleBagFetchException

jak wyjaśniłem w poprzednim artykule o najbardziej wydajnym typie danych dla asocjacji to-many, wewnętrzne nazewnictwo typów kolekcji Hibernate jest dość mylące. Hibernate nazywa go workiem, jeśli elementy w Javie.util.Lista jest nieuporządkowana. Jeśli są uporządkowane, nazywa się to listą.

więc, w zależności od mapowania, Java.util.Lista może być traktowana jako torba lub Lista. Ale nie martw się, w prawdziwym życiu, to nie jest tak mylące, jak mogłoby się wydawać. Zdefiniowanie porządku Stowarzyszenia wymaga dodatkowej adnotacji i jest prawie zawsze narzutem. Dlatego powinieneś tego unikać i dlaczego co najmniej 90% mapowań asocjacyjnych używających Javy.util.Lista i że widziałem w prawdziwych projektach są nieuporządkowane. Hibernate traktuje je jak worek.

Oto prosty model domeny, w którym Hibernate traktuje recenzje i autorów książki jak torby.

@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }

Jeśli spróbujesz pobrać wiele z tych toreb w zapytaniu JPQL, tworzysz produkt kartezjański.

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

może to spowodować problemy z wydajnością. Hibernate stara się również odróżnić informacje, które mają być powielone od informacji, które zostały powielone z powodu iloczynu kartezjańskiego. Z tego powodu Hibernate rzuca MultipleBagFetchException.

java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: 

naprawianie MultipleBagFetchException

Możesz znaleźć wiele pytań na temat tego wyjątku i różnych rozwiązań, aby go uniknąć. Ale wiele z nich mają nieoczekiwane skutki uboczne. Jedyne poprawki 2, pomiędzy którymi powinieneś wybrać, to te, które opiszę w kolejnych sekcjach. To, który z nich jest dla ciebie najlepszy, zależy od wielkości iloczynu kartezjańskiego, który mogą utworzyć Twoje zapytania:

  1. jeśli wszystkie Twoje skojarzenia zawierają tylko niewielką liczbę elementów, utworzony iloczyn kartezjański będzie stosunkowo mały. W takich sytuacjach można zmienić typy atrybutów mapujących skojarzenia do środowiska java.util.Gotowi. Hibernate może następnie pobrać wiele asocjacji w jednym zapytaniu.
  2. jeśli przynajmniej jedno z Twoich skojarzeń zawiera dużo elementów, Twój iloczyn kartezjański stanie się zbyt duży, aby pobrać go efektywnie w 1 zapytaniu. Następnie należy użyć wielu zapytań, które otrzymują różne części wymaganego wyniku.

jak zawsze optymalizacja wydajności aplikacji wymaga wyboru między różnymi kompromisami i nie ma jednego uniwersalnego podejścia. Wydajność każdej opcji zależy od wielkości iloczynu kartezjańskiego i liczby wykonywanych zapytań. Dla stosunkowo małego produktu kartezjańskiego uzyskanie wszystkich informacji za pomocą 1 zapytania zapewnia najlepszą wydajność. Jeśli iloczyn kartezjański osiągnie określony rozmiar, lepiej podzielić go na wiele zapytań.

dlatego pokażę Ci obie opcje, abyś mógł wybrać tę, która pasuje do Twojej aplikacji.

Opcja 1: użyj zestawu zamiast listy

najprostszym sposobem na naprawienie wyjątku MultipleBagFetchException jest zmiana typu atrybutów, które mapują Twoje skojarzenia do-many do Javy.util.Gotowi. To tylko niewielka zmiana w mapowaniu i nie musisz zmieniać kodu biznesowego.

@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }

jak wyjaśniono wcześniej, jeśli teraz wykonasz to samo zapytanie, co pokazałem wcześniej, aby otrzymać książkę ze wszystkimi jej autorami i recenzjami, twój zestaw wyników będzie zawierał produkt kartezjański. Rozmiar tego produktu zależy od liczby wybranych książek oraz liczby powiązanych autorów i recenzji.

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

tutaj możesz zobaczyć wygenerowane zapytanie SQL. Aby uzyskać wszystkie wymagane skojarzenia, Hibernate musi wybrać wszystkie kolumny odwzorowane przez te podmioty. W połączeniu z iloczynem kartezjańskim utworzonym przez 3 połączenia wewnętrzne może to stać się problemem wydajnościowym.

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

za każdym razem, gdy piszesz takie zapytanie, musisz również pamiętać, że Hibernate nie ukrywa, że zbiór wyników zawiera produkt. To zapytanie zwraca każdą książkę wiele razy. Liczba odniesień do tego samego obiektu książki jest obliczana przez liczbę autorów pomnożoną przez liczbę recenzji. Możesz tego uniknąć, dodając słowo kluczowe DISTINCT do klauzuli select i ustawiając hibernację podpowiedzi zapytania.zapytanie.passDistinctThrough to false.

względy wydajności

w tym przykładzie moje zapytanie wybiera tylko 1 książkę, a większość książek została napisana przez 1-3 autorów. Tak więc, nawet jeśli baza danych zawiera kilka recenzji dla tej książki, produkt kartezjański nadal będzie stosunkowo niewielki.

opierając się na tych założeniach, szybsze może być zaakceptowanie nieefektywności iloczynu kartezjańskiego w celu zmniejszenia liczby zapytań. To może się zmienić, jeśli twój iloczyn kartezjański stanie się większy, ponieważ wybierzesz ogromną liczbę książek lub jeśli średnia książka została napisana przez kilkudziesięciu autorów.

Opcja 2: podziel go na wiele zapytań

pobieranie ogromnych produktów kartezjańskich w 1 zapytaniu jest nieefektywne. Wymaga to dużej ilości zasobów w bazie danych i niepotrzebnie obciąża Twoją sieć. Hibernate i sterownik JDBC również muszą wydać więcej zasobów, aby obsłużyć wynik zapytania.

możesz tego uniknąć, wykonując wiele zapytań, które pobierają różne części wymaganego wykresu encji. W przykładzie tego postu chciałbym pobrać książki ze wszystkimi ich autorami w zapytaniu 1, A książki ze wszystkimi recenzjami w zapytaniu 2. Jeśli Wykres wymaganych elementów jest bardziej złożony, może być konieczne użycie większej liczby zapytań lub uzyskanie większej liczby skojarzeń z każdym z nich.

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

jak wyjaśniłem w poprzednim poście, Hibernate zapewnia, że w każdej sesji istnieje tylko 1 obiekt entity, który reprezentuje określony rekord w bazie danych. Można go użyć do efektywnego rozwiązywania odwołań do kluczy obcych lub do umożliwienia Hibernate scalania wyników wielu zapytań.

Jeśli spojrzysz na poniższe wyjście dziennika, zobaczysz, że listy zwracane przez oba zapytania zawierają dokładnie ten sam obiekt. W obu przypadkach Obiekty książki mają referencję @1F.

gdy Hibernate przetworzył wynik drugiego zapytania, sprawdzał dla każdego rekordu, czy bufor pierwszego poziomu zawiera już obiekt dla tej encji Książki. Następnie ponownie użył tego obiektu i dodał zwróconą recenzję do zmapowanego skojarzenia.

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

względy wydajności

Jeśli korzystasz z wielu zapytań, aby uzyskać wymagany Wykres encji, unikasz tworzenia ogromnego produktu kartezjańskiego. Zmniejsza to obciążenie wszystkich zaangażowanych systemów i ułatwia zapewnienie dobrej wydajności dla wszystkich zapytań.

ale niekoniecznie oznacza to, że takie podejście jest szybsze niż wariant 1. Wykonujesz teraz więcej zapytań niż wcześniej. Każdy z nich wymaga przelotu bazy danych i tworzy pewne narzuty zarządzania w bazie danych, np. w celu utworzenia planu wykonania. Z tego powodu opcja ta jest szybsza niż opcja 1, jeśli rozmiar iloczynu kartezjańskiego tworzy większy narzut niż wykonanie wielu zapytań.

wniosek

jak widzieliście w tym artykule, możesz rozwiązać MultipleBagFetchException Hibernate na 2 sposoby:

  • możesz zmienić typ danych atrybutu, który mapuje skojarzenia i pobrać wszystkie informacje w 1 zapytaniu. Wynikiem tego zapytania jest iloczyn kartezjański. Tak długo, jak ten produkt nie staje się zbyt duży, takie podejście jest proste i skuteczne.
  • możesz użyć wielu zapytań, aby pobrać wymagany Wykres encji. Pozwala to uniknąć ogromnego iloczynu kartezjańskiego i jest lepszym podejściem, jeśli potrzebujesz pobrać ogromną ilość danych.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.