Sie haben wahrscheinlich gelernt, dass Sie FetchType verwenden sollten.FAUL für alle Ihre Verbände. Es stellt sicher, dass Hibernate eine Zuordnung initialisiert, wenn Sie sie verwenden, und verbringt keine Zeit damit, Daten abzurufen, die Sie nicht benötigen.
Leider führt dies zu einem neuen Problem. Sie müssen jetzt eine JOIN FETCH Klausel oder einen EntityGraph verwenden, um die Zuordnung abzurufen, wenn Sie sie benötigen. Andernfalls tritt das Problem n + 1 select auf, das schwerwiegende Leistungsprobleme oder eine LazyInitializationException verursacht. Wenn Sie dies für mehrere Zuordnungen tun, löst Hibernate möglicherweise eine MultipleBagFetchException aus.
In diesem Artikel werde ich erklären, wann der Ruhezustand diese Ausnahme auslöst, und Ihnen Ihre 2 besten Optionen zeigen, um das Problem zu beheben. Eine davon eignet sich hervorragend für Assoziationen mit einer kleinen Kardinalität und die andere für Assoziationen, die viele Elemente enthalten. Schauen wir uns also beide an und wählen Sie diejenige aus, die zu Ihrer Anwendung passt.
Ursache der MultipleBagFetchException
Wie ich in einem früheren Artikel über den effizientesten Datentyp für eine to-Many-Zuordnung erläutert habe, ist die interne Benennung der Auflistungstypen in Hibernate ziemlich verwirrend. Hibernate nennt es eine Tasche, wenn die Elemente in Ihrem Java.util.Liste sind ungeordnet. Wenn sie geordnet sind, nennt man es eine Liste.
Also, abhängig von Ihrem Mapping, ein Java.util.Liste kann als eine Tasche oder eine Liste behandelt werden. Aber keine Sorge, im wirklichen Leben ist das nicht so verwirrend, wie es scheinen mag. Das Definieren der Reihenfolge einer Zuordnung erfordert eine zusätzliche Anmerkung und ist fast immer ein Overhead. Deshalb sollten Sie es vermeiden und warum mindestens 90% der Assoziationszuordnungen ein Java verwenden.util.Liste und dass ich in realen Projekten gesehen habe, sind ungeordnet. Hibernate behandelt sie also wie eine Tasche.
Hier ist ein einfaches Domänenmodell, in dem Hibernate die Rezensionen und die Autoren eines Buches als Taschen behandelt.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
Wenn Sie versuchen, mehrere dieser Taschen in einer JPQL-Abfrage abzurufen, erstellen Sie ein kartesisches Produkt.
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();
Dies kann zu Leistungsproblemen führen. Hibernate hat auch Schwierigkeiten, zwischen Informationen zu unterscheiden, die dupliziert werden sollen, und Informationen, die aufgrund des kartesischen Produkts dupliziert wurden. Aus diesem Grund löst Hibernate eine MultipleBagFetchException aus.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
Beheben der MultipleBagFetchException
Sie können viele Fragen zu dieser Ausnahme und verschiedene Lösungen finden, um sie zu vermeiden. Aber viele von ihnen kommen mit unerwarteten Nebenwirkungen. Die einzigen 2 Korrekturen, zwischen denen Sie wählen sollten, sind diejenigen, die ich in den folgenden Abschnitten beschreiben werde. Welches davon für Sie am besten geeignet ist, hängt von der Größe des kartesischen Produkts ab, das Ihre Abfragen möglicherweise erstellen:
- Wenn alle Ihre Zuordnungen nur eine kleine Anzahl von Elementen enthalten, ist das erstellte kartesische Produkt relativ klein. In diesen Situationen können Sie die Typen der Attribute ändern, die Ihre Zuordnungen einem Java zuordnen.util.Setzen. Hibernate kann dann mehrere Zuordnungen in 1 Abfrage abrufen.
- Wenn mindestens eine Ihrer Assoziationen viele Elemente enthält, wird Ihr kartesisches Produkt zu groß, um es in 1 Abfrage effizient abzurufen. Sie sollten dann mehrere Abfragen verwenden, die unterschiedliche Teile des erforderlichen Ergebnisses erhalten.
Wie immer müssen Sie bei der Optimierung der Leistung Ihrer Anwendung zwischen verschiedenen Kompromissen wählen, und es gibt keinen einheitlichen Ansatz. Die Leistung jeder Option hängt von der Größe des kartesischen Produkts und der Anzahl der ausgeführten Abfragen ab. Bei einem relativ kleinen kartesischen Produkt bietet das Abrufen aller Informationen mit 1 Abfrage die beste Leistung. Wenn das kartesische Produkt eine bestimmte Größe erreicht, sollten Sie es besser in mehrere Abfragen aufteilen.
Deshalb zeige ich Ihnen beide Optionen, damit Sie diejenige auswählen können, die zu Ihrer Anwendung passt.
Option 1: Verwenden Sie ein Set anstelle einer Liste
Der einfachste Ansatz, um die MultipleBagFetchException zu beheben, besteht darin, den Typ der Attribute zu ändern, die Ihre zu vielen Zuordnungen einem Java zuordnen.util.Setzen. Dies ist nur eine kleine Änderung in Ihrer Zuordnung, und Sie müssen Ihren Geschäftscode nicht ändern.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
Wie bereits erläutert, enthält Ihre Ergebnismenge ein kartesisches Produkt, wenn Sie jetzt dieselbe Abfrage ausführen, die ich Ihnen zuvor gezeigt habe, um das Buch mit all seinen Autoren und Rezensionen zu erhalten. Die Größe dieses Produkts hängt von der Anzahl der ausgewählten Bücher und der Anzahl der zugehörigen Autoren und Rezensionen ab.
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();
Hier sehen Sie die generierte SQL-Abfrage. Um alle angeforderten Zuordnungen zu erhalten, muss Hibernate alle Spalten auswählen, die diesen Entitäten zugeordnet sind. In Kombination mit dem kartesischen Produkt, das durch die 3 INNEREN JOINs erstellt wird, kann dies zu einem Leistungsproblem werden.
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
Wenn Sie eine solche Abfrage schreiben, müssen Sie auch bedenken, dass Hibernate nicht verbirgt, dass die Ergebnismenge ein Produkt enthält. Diese Abfrage gibt jedes Buch mehrmals zurück. Die Anzahl der Verweise auf dasselbe Buchobjekt berechnet sich aus der Anzahl der Autoren multipliziert mit der Anzahl der Rezensionen. Sie können dies vermeiden, indem Sie Ihrer select Klausel das DISTINCT Schlüsselwort hinzufügen und den Abfragehinweis hibernate .Abfrage.passDistinctThrough auf false.
Leistungsüberlegungen
In diesem Beispiel wählt meine Abfrage nur 1 Buch aus, und die meisten Bücher wurden von 1-3 Autoren geschrieben. Selbst wenn die Datenbank mehrere Rezensionen für dieses Buch enthält, ist das kartesische Produkt immer noch relativ klein.
Basierend auf diesen Annahmen könnte es schneller sein, die Ineffizienz des kartesischen Produkts zu akzeptieren, um die Anzahl der Abfragen zu reduzieren. Dies kann sich ändern, wenn Ihr kartesisches Produkt größer wird, weil Sie eine große Anzahl von Büchern auswählen oder wenn Ihr durchschnittliches Buch von einigen Dutzend Autoren geschrieben wurde.
Option 2: Aufteilen in mehrere Abfragen
Das Abrufen großer kartesischer Produkte in 1 Abfrage ist ineffizient. Es erfordert viele Ressourcen in Ihrer Datenbank und belastet Ihr Netzwerk unnötig. Hibernate und Ihr JDBC-Treiber müssen auch mehr Ressourcen aufwenden, um das Abfrageergebnis zu verarbeiten.
Sie können dies vermeiden, indem Sie mehrere Abfragen durchführen, die verschiedene Teile des erforderlichen Diagramms von Entitäten abrufen. Im Beispiel dieses Beitrags würde ich die Bücher mit all ihren Autoren in 1 Abfrage und die Bücher mit all ihren Rezensionen in einer 2. Abfrage abrufen. Wenn Ihr Diagramm der erforderlichen Entitäten komplexer ist, müssen Sie möglicherweise mehr Abfragen verwenden oder mehr Zuordnungen zu jedem von ihnen abrufen.
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());
Wie ich im Beitrag der letzten Woche erklärt habe, stellt Hibernate sicher, dass es innerhalb jeder Sitzung nur 1 Entitätsobjekt gibt, das einen bestimmten Datensatz in der Datenbank darstellt. Sie können dies verwenden, um Fremdschlüsselreferenzen effizient aufzulösen oder Hibernate die Ergebnisse mehrerer Abfragen zusammenführen zu lassen.
Wenn Sie sich die folgende Protokollausgabe ansehen, können Sie feststellen, dass die von beiden Abfragen zurückgegebenen Listen genau dasselbe Objekt enthalten. In beiden Fällen haben die Buchobjekte die Referenz @1f .
Als Hibernate das Ergebnis der 2. Abfrage verarbeitete, wurde für jeden Datensatz überprüft, ob der Cache der 1. Ebene bereits ein Objekt für diese Buchentität enthielt. Anschließend wurde dieses Objekt wiederverwendet und die zurückgegebene Überprüfung der zugeordneten Zuordnung hinzugefügt.
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
Leistungsüberlegungen
Wenn Sie mehrere Abfragen verwenden, um das erforderliche Diagramm von Entitäten abzurufen, vermeiden Sie die Erstellung eines riesigen kartesischen Produkts. Dies entlastet alle beteiligten Systeme und erleichtert die Sicherstellung einer guten Performance für alle Abfragen.
Aber das bedeutet nicht unbedingt, dass dieser Ansatz schneller ist als Option 1. Sie führen jetzt mehr Abfragen als zuvor aus. Jeder von ihnen erfordert einen Datenbank-Roundtrip und erzeugt einen gewissen Verwaltungsaufwand in der Datenbank, z. B. um einen Ausführungsplan zu erstellen. Aus diesem Grund ist diese Option nur dann schneller als Option 1, wenn die Größe des kartesischen Produkts einen größeren Overhead verursacht als die Ausführung mehrerer Abfragen.
Fazit
Wie Sie in diesem Artikel gesehen haben, können Sie die MultipleBagFetchException von Hibernate auf 2 Arten lösen:
- Sie können den Datentyp des Attributs ändern, das die Zuordnungen zuordnet, und alle Informationen in 1 Abfrage abrufen. Das Ergebnis dieser Abfrage ist ein kartesisches Produkt. Solange dieses Produkt nicht zu groß wird, ist dieser Ansatz einfach und effizient.
- Sie können mehrere Abfragen verwenden, um die erforderliche Anzahl von Entitäten abzurufen. Dies vermeidet ein riesiges kartesisches Produkt und ist der bessere Ansatz, wenn Sie eine große Datenmenge abrufen müssen.