Du har förmodligen lärt dig att du ska använda FetchType.Lat för alla dina föreningar. Det säkerställer att Hibernate initierar en förening när du använder den och inte spenderar någon tid på att få data du inte behöver.
tyvärr introducerar detta en ny fråga. Du måste nu använda en JOIN FETCH-klausul eller en EntityGraph för att hämta föreningen om du behöver den. Annars kommer du att uppleva n+1 select-problemet, vilket orsakar allvarliga prestandaproblem eller en LazyInitializationException. Om du gör det för flera föreningar kan Hibernate kasta en MultipleBagFetchException.
i den här artikeln kommer jag att förklara när Hibernate kastar detta undantag och visa dig dina 2 bästa alternativ för att fixa det. En av dem är en bra passform för föreningar med en liten kardinalitet och den andra för föreningar som innehåller många element. Så, låt oss ta en titt på dem båda, och du väljer den som passar din ansökan.
orsaken till MultipleBagFetchException
som jag förklarade i en tidigare artikel om den mest effektiva datatypen för en to-many-förening är hibernates interna namngivning av samlingstyperna ganska förvirrande. Hibernate kallar det en väska, om elementen i din java.util.Listan är oordnad. Om de beställs kallas det en lista.
så, beroende på din mappning, en java.util.Listan kan behandlas som en väska eller en lista. Men oroa dig inte, i verkligheten är det inte så förvirrande som det kan tyckas. Att definiera en förenings ordning kräver en ytterligare anteckning och är nästan alltid en overhead. Det är därför du bör undvika det och varför minst 90% av associeringsmappningarna som använder en java.util.Lista och som jag har sett i verkliga projekt är oordnade. Så, Hibernate behandlar dem som en väska.
Här är en enkel domänmodell där Hibernate behandlar recensionerna och författarna till en bok som påsar.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
om du försöker hämta flera av dessa påsar i en JPQL-fråga skapar du en kartesisk 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();
detta kan skapa prestandaproblem. Hibernate kämpar också för att skilja mellan information som ska dupliceras och information som dupliceras på grund av den kartesiska produkten. På grund av det, Hibernate kastar en MultipleBagFetchException.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
fastställande av MultipleBagFetchException
Du kan hitta massor av frågor om detta undantag och olika lösningar för att undvika det. Men många av dem kommer med oväntade biverkningar. De enda 2-korrigeringarna mellan vilka du ska välja är de som jag kommer att beskriva i följande avsnitt. Vilken av dem som är bäst för dig beror på storleken på den kartesiska produkten som dina frågor kan skapa:
- Om alla dina föreningar bara innehåller ett litet antal element kommer den skapade kartesiska produkten att vara relativt liten. I dessa situationer kan du ändra de typer av attribut som mappar dina associationer till en java.util.Ställa. Hibernate kan sedan hämta flera föreningar i 1 fråga.
- Om minst en av dina föreningar innehåller många element blir din kartesiska produkt för stor för att hämta den effektivt i 1 fråga. Du bör sedan använda flera frågor som får olika delar av det önskade resultatet.
som alltid kräver optimering av prestanda för din applikation att du väljer mellan olika avvägningar, och det finns ingen one-size-fits-all-strategi. Prestandan för varje alternativ beror på storleken på den kartesiska produkten och antalet frågor du kör. För en relativt liten kartesisk produkt, att få all information med 1 Fråga ger dig den bästa prestandan. Om den kartesiska produkten når en viss storlek, bör du bättre dela upp den i flera frågor.
det är därför jag kommer att visa dig båda alternativen så att du kan välja den som passar din ansökan.
alternativ 1: använd en uppsättning istället för en lista
det enklaste sättet att fixa MultipleBagFetchException är att ändra typen av attribut som mappar dina till-många föreningar till en java.util.Ställa. Det här är bara en liten förändring i din kartläggning, och du behöver inte ändra din företagskod.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
som förklarats tidigare, om du nu utför samma fråga som jag visade dig tidigare för att få boken med alla dess författare och recensioner, kommer din resultatuppsättning att innehålla en kartesisk produkt. Storleken på den produkten beror på antalet böcker du väljer och antalet associerade författare och recensioner.
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();
Här kan du se den genererade SQL-frågan. För att få alla begärda föreningar måste Hibernate välja alla kolumner som mappas av dessa enheter. I kombination med den kartesiska produkten som skapats av de 3 Inre föreningarna kan detta bli ett prestandaproblem.
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
När du skriver en sådan fråga måste du också komma ihåg att Hibernate inte döljer att resultatuppsättningen innehåller en produkt. Denna fråga returnerar varje bok flera gånger. Antalet referenser till samma Bokobjekt beräknas med antalet författare multiplicerat med antalet recensioner. Du kan undvika det genom att lägga till det distinkta sökordet i din select-klausul och genom att ställa in frågetipset hibernate.fråga.passDistinctThrough till false.
Prestandaöverväganden
i det här exemplet väljer min fråga bara 1 bok, och de flesta böcker har skrivits av 1-3 författare. Så även om databasen innehåller flera recensioner för den här boken kommer den kartesiska produkten fortfarande att vara relativt liten.
baserat på dessa antaganden kan det vara snabbare att acceptera ineffektiviteten hos den kartesiska produkten för att minska antalet frågor. Detta kan förändras om din kartesiska produkt blir större eftersom du väljer ett stort antal böcker eller om din genomsnittliga bok har skrivits av några dussin författare.
alternativ 2: Dela upp det i flera frågor
hämta enorma kartesiska produkter i 1 fråga är ineffektivt. Det kräver mycket resurser i din databas och lägger onödig belastning på ditt nätverk. Viloläge och din JDBC-drivrutin måste också spendera mer resurser för att hantera frågeresultatet.
Du kan undvika det genom att utföra flera frågor som hämtar olika delar av den önskade grafen för enheter. I exemplet med det här inlägget skulle jag hämta böckerna med alla sina författare i 1 fråga och böckerna med alla sina recensioner i en 2: A fråga. Om grafen över obligatoriska enheter är mer komplex kan du behöva använda fler frågor eller hämta fler associationer med var och en av dem.
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());
som jag förklarade i förra veckans inlägg säkerställer Hibernate att inom varje Session finns det bara 1 entitetsobjekt som representerar en specifik post i databasen. Du kan använda det för att lösa främmande nyckelreferenser effektivt eller för att låta viloläge slå samman resultaten av flera frågor.
om du tittar på följande loggutmatning kan du se att listorna som returneras av båda frågorna innehåller exakt samma objekt. I båda fallen har Bokobjekten referensen @1F.
När Hibernate behandlade resultatet av 2: a frågan, kontrollerade den för varje post om 1: a nivåcachen redan innehöll ett objekt för den Bokenheten. Det återanvände sedan det objektet och lade till den returnerade granskningen till den mappade föreningen.
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
Prestandaöverväganden
Om du använder flera frågor för att få den önskade grafen över enheter undviker du skapandet av en enorm kartesisk produkt. Detta minskar belastningen på alla inblandade system och gör det lättare att säkerställa en bra prestanda för alla frågor.
men det betyder inte nödvändigtvis att detta tillvägagångssätt är snabbare än alternativ 1. Du utför nu fler frågor än tidigare. Var och en av dem kräver en databas tur och retur och skapar vissa hanteringskostnader i databasen, t.ex. för att skapa en exekveringsplan. På grund av det är det här alternativet bara snabbare än alternativ 1, om storleken på den kartesiska produkten skapar en större kostnad än utförandet av flera frågor.
slutsats
som du har sett i den här artikeln kan du lösa hibernates MultipleBagFetchException på 2 sätt:
- Du kan ändra datatypen för attributet som kartlägger föreningarna och hämta all information i 1 fråga. Resultatet av den frågan är en kartesisk produkt. Så länge denna produkt inte blir för stor är detta tillvägagångssätt enkelt och effektivt.
- Du kan använda flera frågor för att hämta den önskade grafen för enheter. Detta undviker en enorm kartesisk produkt och är det bättre tillvägagångssättet om du behöver hämta en enorm mängd data.