Probabilmente hai imparato che dovresti usare FetchType.PIGRO per tutte le tue associazioni. Assicura che Hibernate inizializzi un’associazione quando la usi e non passi del tempo a ottenere dati che non ti servono.
Sfortunatamente, questo introduce un nuovo problema. Ora è necessario utilizzare una clausola JOIN FETCH o un EntityGraph per recuperare l’associazione se ne avete bisogno. In caso contrario, si verificherà il problema di selezione n+1, che causa gravi problemi di prestazioni o una LazyInitializationException. Se lo fai per più associazioni, Hibernate potrebbe generare una MultipleBagFetchException.
In questo articolo, spiegherò quando Hibernate lancia questa eccezione e ti mostrerò le tue 2 migliori opzioni per risolverlo. Uno di questi è adatto per associazioni con una piccola cardinalità e l’altro per associazioni che contengono molti elementi. Quindi, diamo un’occhiata a entrambi, e scegli quello che si adatta alla tua applicazione.
Causa della MultipleBagFetchException
Come ho spiegato in un precedente articolo sul tipo di dati più efficiente per un’associazione a molti, la denominazione interna di Hibernate dei tipi di raccolta è piuttosto confusa. Hibernate lo chiama una borsa, se gli elementi nel tuo java.util.Elenco non sono ordinati. Se sono ordinati, si chiama una lista.
Quindi, a seconda della mappatura, un java.util.La lista può essere trattata come una borsa o una lista. Ma non preoccuparti, nella vita reale, questo non è così confuso come potrebbe sembrare. Definire l’ordine di un’associazione richiede un’annotazione aggiuntiva ed è quasi sempre un overhead. Ecco perché dovresti evitarlo e perché almeno il 90% delle mappature di associazione che utilizzano un java.util.Elenco e che ho visto in progetti reali non sono ordinati. Quindi, Hibernate li tratta come una borsa.
Ecco un semplice modello di dominio in cui Hibernate tratta le recensioni e gli autori di un libro come Borse.
@Entitypublic class Book { @ManyToMany private List authors = new ArrayList(); @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... }
Se si tenta di recuperare più di questi sacchetti in una query JPQL, si crea un prodotto cartesiano.
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();
Questo può creare problemi di prestazioni. Hibernate si sforza anche di distinguere tra le informazioni che dovrebbero essere duplicate e le informazioni che sono state duplicate a causa del prodotto cartesiano. Per questo motivo, Hibernate genera una MultipleBagFetchException.
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
Fissando la MultipleBagFetchException
Puoi trovare molte domande su questa eccezione e varie soluzioni per evitarla. Ma molti di loro hanno effetti collaterali inaspettati. Le uniche 2 correzioni tra le quali dovresti scegliere sono quelle che descriverò nelle sezioni seguenti. Quale di questi è il migliore per te dipende dalla dimensione del prodotto cartesiano che le tue query potrebbero creare:
- Se tutte le tue associazioni contengono solo un piccolo numero di elementi, il prodotto cartesiano creato sarà relativamente piccolo. In queste situazioni, è possibile modificare i tipi di attributi che associano le associazioni a un java.util.Impostare. Hibernate può quindi recuperare più associazioni in 1 query.
- Se almeno una delle tue associazioni contiene molti elementi, il tuo prodotto cartesiano diventerà troppo grande per recuperarlo in modo efficiente in 1 query. È quindi necessario utilizzare più query che ottengono parti diverse del risultato richiesto.
Come sempre, l’ottimizzazione delle prestazioni della tua applicazione richiede di scegliere tra diversi compromessi e non esiste un approccio adatto a tutti. Le prestazioni di ciascuna opzione dipendono dalle dimensioni del prodotto cartesiano e dal numero di query in esecuzione. Per un prodotto cartesiano relativamente piccolo, ottenere tutte le informazioni con 1 query fornisce le migliori prestazioni. Se il prodotto cartesiano raggiunge una certa dimensione, è meglio dividerlo in più query.
Ecco perché ti mostrerò entrambe le opzioni in modo da poter scegliere quella che si adatta alla tua applicazione.
Opzione 1: Usa un Set invece di un elenco
L’approccio più semplice per correggere MultipleBagFetchException è cambiare il tipo degli attributi che mappano le tue associazioni to-many a un java.util.Impostare. Questa è solo una piccola modifica nella mappatura e non è necessario modificare il codice aziendale.
@Entitypublic class Book { @ManyToMany private Set authors = new HashSet(); @OneToMany(mappedBy = "book") private Set reviews = new HashSet(); ... }
Come spiegato in precedenza, se ora esegui la stessa query che ti ho mostrato prima per ottenere il Libro con tutti i suoi autori e recensioni, il tuo set di risultati conterrà un prodotto cartesiano. La dimensione del prodotto dipende dal numero di Libri selezionati e dal numero di autori e recensioni associati.
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();
Qui puoi vedere la query SQL generata. Per ottenere tutte le associazioni richieste, Hibernate deve selezionare tutte le colonne mappate da queste entità. In combinazione con il prodotto cartesiano creato dai 3 INNER JOIN, questo può diventare un problema di prestazioni.
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
Ogni volta che scrivi una query di questo tipo, devi anche tenere presente che Hibernate non nasconde che il set di risultati contiene un prodotto. Questa query restituisce ogni libro più volte. Il numero di riferimenti allo stesso oggetto Libro viene calcolato dal numero di Autori moltiplicato per il numero di Recensioni. Puoi evitarlo aggiungendo la parola chiave DISTINCT alla clausola select e impostando il suggerimento di query hibernate.query.passDistinctThrough a false.
Considerazioni sulle prestazioni
In questo esempio, la mia query seleziona solo 1 libro e la maggior parte dei libri è stata scritta da 1-3 Autori. Quindi, anche se il database contiene diverse recensioni per questo libro, il prodotto cartesiano sarà ancora relativamente piccolo.
Sulla base di queste ipotesi, potrebbe essere più veloce accettare l’inefficienza del prodotto cartesiano per ridurre il numero di query. Questo potrebbe cambiare se il tuo prodotto cartesiano diventa più grande perché selezioni un numero enorme di libri o se il tuo libro medio è stato scritto da poche decine di autori.
Opzione 2: Dividerlo in più query
Il recupero di enormi prodotti cartesiani in 1 query è inefficiente. Si richiede un sacco di risorse nel database e mette carico inutile sulla rete. Hibernate e il driver JDBC devono anche spendere più risorse per gestire il risultato della query.
È possibile evitarlo eseguendo più query che recuperano parti diverse del grafico richiesto delle entità. Nell’esempio di questo post, vorrei recuperare i libri con tutti i loro autori in 1 query e i Libri con tutte le loro recensioni in una 2a query. Se il grafico delle entità richieste è più complesso, potrebbe essere necessario utilizzare più query o recuperare più associazioni con ciascuna di esse.
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());
Come ho spiegato nel post della scorsa settimana, Hibernate assicura che all’interno di ogni sessione, ci sia solo 1 oggetto entità che rappresenta un record specifico nel database. È possibile utilizzarlo per risolvere i riferimenti a chiavi esterne in modo efficiente o per consentire a Hibernate di unire i risultati di più query.
Se dai un’occhiata al seguente output del log, puoi vedere che gli elenchi restituiti da entrambe le query contengono esattamente lo stesso oggetto. In entrambi i casi, gli oggetti del Libro hanno il riferimento @1f.
Quando Hibernate ha elaborato il risultato della 2a query, ha controllato per ogni record se la cache di 1 ° livello conteneva già un oggetto per quell’entità del Libro. Ha quindi riutilizzato quell’oggetto e aggiunto la revisione restituita all’associazione mappata.
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
Considerazioni sulle prestazioni
Se si utilizzano più query per ottenere il grafico richiesto delle entità, si evita la creazione di un enorme prodotto cartesiano. Ciò riduce il carico su tutti i sistemi coinvolti e rende più facile garantire una buona prestazione per tutte le query.
Ma ciò non significa necessariamente che questo approccio sia più veloce dell’opzione 1. Ora esegui più query di prima. Ognuno di essi richiede un roundtrip del database e crea un sovraccarico di gestione nel database, ad esempio per creare un piano di esecuzione. A causa di ciò, questa opzione è solo più veloce dell’opzione 1, se la dimensione del prodotto cartesiano crea un overhead maggiore rispetto all’esecuzione di più query.
Conclusione
Come hai visto in questo articolo, puoi risolvere la MultipleBagFetchException di Hibernate in 2 modi:
- Puoi cambiare il tipo di dati dell’attributo che mappa le associazioni e recuperare tutte le informazioni in 1 query. Il risultato di tale query è un prodotto cartesiano. Finché questo prodotto non diventa troppo grande, questo approccio è semplice ed efficiente.
- È possibile utilizzare più query per recuperare il grafico richiesto di entità. Ciò evita un enorme prodotto cartesiano ed è l’approccio migliore se è necessario recuperare un’enorme quantità di dati.