probabil ați învățat că ar trebui să utilizați FetchType.Leneș pentru toate asociațiile tale. Se asigură că Hibernate inițializează o asociere atunci când o utilizați și nu petrece nici un moment obtinerea de date nu aveți nevoie.
Din păcate, aceasta introduce o nouă problemă. Acum trebuie să utilizați o clauză join FETCH sau un EntityGraph pentru a prelua Asociația dacă aveți nevoie de ea. În caz contrar, veți experimenta problema N+1 select, care provoacă probleme grave de performanță sau o LazyInitializationException. Dacă faci asta pentru mai multe asociații, Hibernate ar putea arunca un MultipleBagFetchException.
în acest articol, vă voi explica când Hibernate aruncă această excepție și vă voi arăta cele mai bune 2 opțiuni pentru a o remedia. Una dintre ele este o potrivire excelentă pentru asociațiile cu o mică cardinalitate, iar cealaltă pentru asociațiile care conțin o mulțime de elemente. Deci, să aruncăm o privire la ambele dintre ele, și alegeți unul care se potrivește cererea dumneavoastră.
cauza MultipleBagFetchException
așa cum am explicat într-un articol anterior despre cel mai eficient tip de date pentru o asociație La mulți, denumirea internă Hibernate a tipurilor de colectare este destul de confuză. Hibernate numește un sac, în cazul în care elementele din java.util.Lista este neordonată. Dacă sunt ordonate, se numește listă.
deci, în funcție de cartografiere, un java.util.Lista poate fi tratată ca o pungă sau o listă. Dar nu vă faceți griji, în viața reală, acest lucru nu este atât de confuz pe cât ar putea părea. Definirea ordinii unei asociații necesită o adnotare suplimentară și este aproape întotdeauna o regie. De aceea ar trebui să o evitați și de ce cel puțin 90% din mapările de asociere care utilizează un java.util.Lista și că am văzut în proiecte reale sunt neordonate. Deci, Hibernate le tratează ca pe o pungă.
Iată un model de domeniu simplu în care Hibernate tratează recenziile și autorii unei cărți ca pungi.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
dacă încercați să preluați mai multe dintre aceste pungi într-o interogare JPQL, creați un produs cartezian.
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();
Acest lucru poate crea probleme de performanță. Hibernate se luptă, de asemenea, să facă diferența între informațiile care se presupune că sunt duplicate și informațiile care au fost duplicate din cauza produsului cartezian. Din acest motiv, Hibernate aruncă un MultipleBagFetchException.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
fixarea MultipleBagFetchException
puteți găsi o mulțime de întrebări cu privire la această excepție și diverse soluții pentru a evita. Dar multe dintre ele vin cu efecte secundare neașteptate. Singurele remedieri 2 dintre care ar trebui să alegeți sunt cele pe care le voi descrie în secțiunile următoare. Care dintre ele este cea mai bună pentru dvs. depinde de dimensiunea produsului cartezian pe care îl pot crea interogările dvs.:
- dacă toate asociațiile dvs. conțin doar un număr mic de elemente, produsul cartezian creat va fi relativ mic. În aceste situații, puteți modifica tipurile de atribute care mapează asociațiile dvs. într-un java.util.Gata. Hibernate poate prelua apoi mai multe asociații în 1 interogare.
- dacă cel puțin una dintre asociațiile dvs. conține o mulțime de elemente, produsul dvs. cartezian va deveni prea mare pentru a-l prelua eficient în 1 interogare. Apoi, ar trebui să utilizați mai multe interogări care obțin diferite părți ale rezultatului dorit.
ca întotdeauna, optimizarea performanței aplicației dvs. necesită alegerea între diferite compromisuri și nu există o abordare unică. Performanța fiecărei opțiuni depinde de dimensiunea produsului cartezian și de numărul de interogări pe care le executați. Pentru un produs cartezian relativ mic, obținerea tuturor informațiilor cu 1 interogare vă oferă cea mai bună performanță. Dacă produsul cartezian atinge o anumită dimensiune, ar trebui să îl împărțiți mai bine în mai multe interogări.
De aceea vă voi arăta ambele opțiuni, astfel încât să o puteți alege pe cea care se potrivește aplicației dvs.
opțiunea 1: utilizați un Set în loc de o listă
cea mai ușoară abordare pentru a remedia MultipleBagFetchException este de a schimba tipul atributelor care mapează asociațiile dvs. la-multe la un java.util.Gata. Aceasta este doar o mică schimbare în cartografiere, și nu aveți nevoie pentru a schimba codul de afaceri.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
după cum am explicat mai devreme, dacă efectuați acum aceeași interogare pe care v-am arătat-o înainte pentru a obține cartea cu toți autorii și recenziile sale, setul dvs. de rezultate va conține un produs cartezian. Dimensiunea acelui produs depinde de numărul de cărți pe care le selectați și de numărul de autori și recenzii asociate.
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();
aici puteți vedea interogarea SQL generată. Pentru a obține toate asociațiile solicitate, Hibernate trebuie să selecteze toate coloanele mapate de aceste entități. În combinație cu produsul cartezian creat de cele 3 îmbinări interioare, aceasta poate deveni o problemă de performanță.
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
ori de câte ori scrieți o astfel de interogare, trebuie să rețineți că Hibernate nu ascunde că setul de rezultate conține un produs. Această interogare returnează fiecare carte de mai multe ori. Numărul de referințe la același obiect de carte este calculat prin numărul de autori înmulțit cu numărul de recenzii. Puteți evita acest lucru adăugând cuvântul cheie DISTINCT la clauza select și setând sugestia de interogare hibernate.interogare.passDistinctThrough la fals.
considerații de performanță
în acest exemplu, interogarea mea selectează doar 1 carte, iar majoritatea cărților au fost scrise de 1-3 autori. Deci, chiar dacă baza de date conține mai multe recenzii pentru această carte, produsul cartezian va fi în continuare relativ mic.
pe baza acestor ipoteze, ar putea fi mai rapid să acceptăm ineficiența produsului cartezian pentru a reduce numărul de interogări. Acest lucru s-ar putea schimba dacă produsul dvs. cartezian devine mai mare, deoarece selectați un număr imens de cărți sau dacă cartea dvs. medie a fost scrisă de câteva zeci de autori.
Opțiunea 2: împărțiți-o în mai multe interogări
preluarea produselor carteziene uriașe în 1 interogare este ineficientă. Este nevoie de o mulțime de resurse în baza de date și pune sarcină inutilă în rețea. Hibernate și driverul JDBC trebuie, de asemenea, să cheltuiască mai multe resurse pentru a gestiona rezultatul interogării.
puteți evita acest lucru efectuând mai multe interogări care aduc diferite părți ale graficului necesar de entități. În exemplul acestui post, aș aduce cărțile cu toți autorii lor în interogarea 1 și cărțile cu toate recenziile lor într-o interogare 2nd. Dacă graficul entităților solicitate este mai complex, poate fi necesar să utilizați mai multe interogări sau să preluați mai multe asociații cu fiecare dintre ele.
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());
după cum am explicat în postarea de săptămâna trecută, Hibernate asigură că în cadrul fiecărei sesiuni există doar 1 obiect entitate care reprezintă o înregistrare specifică în baza de date. Puteți utiliza acest lucru pentru a rezolva eficient referințele cheie străine sau pentru a permite Hibernate să îmbine rezultatele mai multor interogări.
dacă aruncați o privire la următoarea ieșire din jurnal, puteți vedea că listele returnate de ambele interogări conțin exact același obiect. În ambele cazuri, obiectele de carte au referința @1F.
când Hibernate a procesat rezultatul interogării a 2-a, a verificat pentru fiecare înregistrare dacă cache-ul de nivel 1 conținea deja un obiect pentru acea entitate de carte. Apoi a reutilizat acel obiect și a adăugat Recenzia returnată Asociației mapate.
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
considerații de performanță
dacă utilizați mai multe interogări pentru a obține graficul necesar de entități, evitați crearea unui produs cartezian imens. Acest lucru reduce sarcina pe toate sistemele implicate și facilitează asigurarea unei performanțe bune pentru toate interogările.
dar asta nu înseamnă neapărat că această abordare este mai rapidă decât opțiunea 1. Acum efectuați mai multe interogări decât înainte. Fiecare dintre ele necesită o bază de date dus-întors și creează unele cheltuieli generale de gestionare în baza de date, de exemplu, pentru a crea un plan de execuție. Datorită acestui fapt, această opțiune este doar mai rapidă decât opțiunea 1, Dacă dimensiunea produsului cartezian creează o regie mai mare decât executarea mai multor interogări.
concluzie
după cum ați văzut în acest articol, puteți rezolva MultipleBagFetchException Hibernate în 2 moduri:
- puteți schimba tipul de date al atributului care mapează asociațiile și de a prelua toate informațiile în 1 interogare. Rezultatul acestei interogări este un produs cartezian. Atâta timp cât acest produs nu devine prea mare, această abordare este simplă și eficientă.
- puteți utiliza mai multe interogări pentru a prelua graficul necesar de entități. Acest lucru evită un produs cartezian imens și este abordarea mai bună dacă trebuie să preluați o cantitate imensă de date.