Guida rapida alla proprietà Hibernate enable_lazy_load_no_trans

1. Panoramica

Durante l'utilizzo del caricamento lento in Hibernate, potremmo incontrare delle eccezioni, dicendo che non c'è sessione.

In questo tutorial, discuteremo come risolvere questi problemi di caricamento lento. Per fare ciò, useremo Spring Boot per esplorare un esempio.

2. Problemi di caricamento lento

Lo scopo del caricamento lento è di risparmiare risorse non caricando oggetti correlati in memoria quando carichiamo l'oggetto principale. Invece, rimandiamo l'inizializzazione delle entità pigre fino al momento in cui sono necessarie. Hibernate utilizza proxy e wrapper di raccolta per implementare il caricamento lento.

Quando si recuperano dati caricati in modo lento, ci sono due passaggi nel processo. In primo luogo, sta popolando l'oggetto principale e in secondo luogo, recuperando i dati all'interno dei suoi proxy. Il caricamento dei dati richiede sempre una sessione aperta in Hibernate.

Il problema sorge quando si verifica il secondo passaggio dopo la chiusura della transazione , il che porta a una LazyInitializationException .

L'approccio consigliato è progettare la nostra applicazione per garantire che il recupero dei dati avvenga in una singola transazione. Tuttavia, questo a volte può essere difficile quando si utilizza un'entità pigra in un'altra parte del codice che non è in grado di determinare cosa è stato caricato o meno.

Hibernate ha una soluzione alternativa, una proprietà enable_lazy_load_no_trans . Attivare questa opzione significa che ogni recupero di un'entità pigra aprirà una sessione temporanea e verrà eseguita all'interno di una transazione separata.

3. Esempio di caricamento lento

Diamo un'occhiata al comportamento del caricamento lento in alcuni scenari.

3.1 Configurazione di entità e servizi

Supponiamo di avere due entità, Utente e Documento . Un utente può avere molti documenti e useremo @OneToMany per descrivere quella relazione. Inoltre, useremo @Fetch (FetchMode.SUBSELECT) per l'efficienza.

Dobbiamo notare che, per impostazione predefinita, @OneToMany ha un tipo di recupero lento.

Definiamo ora la nostra entità Utente :

@Entity public class User { // other fields are omitted for brevity @OneToMany(mappedBy = "userId") @Fetch(FetchMode.SUBSELECT) private List docs = new ArrayList(); }

Successivamente, abbiamo bisogno di un livello di servizio con due metodi per illustrare le diverse opzioni. Uno di questi è annotato come @Transactional . Entrambi i metodi eseguono la stessa logica contando tutti i documenti di tutti gli utenti:

@Service public class ServiceLayer { @Autowired private UserRepository userRepository; @Transactional(readOnly = true) public long countAllDocsTransactional() { return countAllDocs(); } public long countAllDocsNonTransactional() { return countAllDocs(); } private long countAllDocs() { return userRepository.findAll() .stream() .map(User::getDocs) .mapToLong(Collection::size) .sum(); } }

Ora, diamo un'occhiata più da vicino ai seguenti tre esempi. Useremo anche SQLStatementCountValidator per comprendere l'efficienza della soluzione, contando il numero di query eseguite.

3.2. Caricamento lento con una transazione circostante

Prima di tutto, usiamo il caricamento lento nel modo consigliato. Quindi, chiameremo il nostro metodo @Transactional nel livello di servizio:

@Test public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(2); }

Come possiamo vedere, questo funziona e si traduce in due viaggi di andata e ritorno al database . Il primo roundtrip seleziona gli utenti e il secondo seleziona i loro documenti.

3.3. Caricamento lento al di fuori di una transazione

Ora, chiamiamo un metodo non transazionale per simulare l'errore che otteniamo senza una transazione circostante:

@Test(expected = LazyInitializationException.class) public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() { serviceLayer.countAllDocsNonTransactional(); }

Come previsto, ciò si traduce in un errore poiché la funzione getDocs dell'utente viene utilizzata al di fuori di una transazione.

3.4. Caricamento lento con transazione automatica

Per risolvere questo problema, possiamo abilitare la proprietà:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Con la proprietà attivata, non otteniamo più un'eccezione LazyInitializationException .

Tuttavia, il conteggio delle query mostra che sono stati effettuati sei viaggi di andata e ritorno al database . Qui, un roundtrip seleziona gli utenti e cinque roundtrip selezionano i documenti per ciascuno dei cinque utenti:

@Test public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsNonTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(EXPECTED_USERS_COUNT + 1); }

Ci siamo imbattuti nel famigerato problema N + 1 , nonostante il fatto che abbiamo impostato una strategia di recupero per evitarlo!

4. Confronto degli approcci

Discutiamo brevemente dei pro e dei contro.

Con la proprietà attivata, non dobbiamo preoccuparci delle transazioni e dei loro confini. Hibernate lo gestisce per noi.

Tuttavia, la soluzione funziona lentamente, perché Hibernate avvia una transazione per noi a ogni recupero.

Funziona perfettamente per le demo e quando non ci interessano i problemi di prestazioni. Questo può essere corretto se utilizzato per recuperare una raccolta che contiene un solo elemento o un singolo oggetto correlato in una relazione uno a uno.

Senza la proprietà, abbiamo un controllo dettagliato delle transazioni e non dobbiamo più affrontare problemi di prestazioni.

Nel complesso, questa non è una funzionalità pronta per la produzione e la documentazione di Hibernate ci avverte:

Sebbene l'abilitazione di questa configurazione possa far scomparire LazyInitializationException , è preferibile utilizzare un piano di recupero che garantisca che tutte le proprietà siano inizializzate correttamente prima della chiusura di Session.

5. conclusione

In questo tutorial, abbiamo esplorato la gestione del caricamento lento.

Abbiamo provato una proprietà Hibernate per aiutare a superare l' eccezione LazyInitializationException . Abbiamo anche visto come riduce l'efficienza e può essere una soluzione praticabile solo per un numero limitato di casi d'uso.

Come sempre, tutti gli esempi di codice sono disponibili su GitHub.