Lavorare con le raccolte di elementi pigri in JPA

Java Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

La specifica JPA fornisce due diverse strategie di recupero: desideroso e pigro. Mentre l'approccio pigro aiuta a evitare di caricare inutilmente dati che non ci servono, a volte abbiamo bisogno di leggere dati non inizialmente caricati in un contesto di persistenza chiuso. Inoltre, l'accesso a raccolte di elementi pigri in un contesto di persistenza chiuso è un problema comune.

In questo tutorial, ci concentreremo su come caricare i dati da raccolte di elementi pigri. Esploreremo tre diverse soluzioni: una che coinvolge il linguaggio di query JPA, un'altra con l'uso di grafici di entità e l'ultima con la propagazione delle transazioni.

2. Il problema della raccolta degli elementi

Per impostazione predefinita, JPA utilizza la strategia di recupero lento nelle associazioni di tipo @ElementCollection . Pertanto, qualsiasi accesso alla raccolta in un contesto di persistenza chiuso risulterà in un'eccezione.

Per comprendere il problema, definiamo un modello di dominio basato sulla relazione tra il dipendente e il suo elenco telefonico:

@Entity public class Employee { @Id private int id; private String name; @ElementCollection @CollectionTable(name = "employee_phone", joinColumns = @JoinColumn(name = "employee_id")) private List phones; // standard constructors, getters, and setters } @Embeddable public class Phone { private String type; private String areaCode; private String number; // standard constructors, getters, and setters }

Il nostro modello specifica che un dipendente può avere molti telefoni. L'elenco telefonico è una raccolta di tipi incorporabili . Usiamo un repository Spring con questo modello:

@Repository public class EmployeeRepository { public Employee findById(int id) { return em.find(Employee.class, id); } // additional properties and auxiliary methods } 

Ora, riproduciamo il problema con un semplice test case JUnit:

public class ElementCollectionIntegrationTest { @Before public void init() { Employee employee = new Employee(1, "Fred"); employee.setPhones( Arrays.asList(new Phone("work", "+55", "99999-9999"), new Phone("home", "+55", "98888-8888"))); employeeRepository.save(employee); } @After public void clean() { employeeRepository.remove(1); } @Test(expected = org.hibernate.LazyInitializationException.class) public void whenAccessLazyCollection_thenThrowLazyInitializationException() { Employee employee = employeeRepository.findById(1); assertThat(employee.getPhones().size(), is(2)); } } 

Questo test genera un'eccezione quando si tenta di accedere all'elenco dei telefoni perché il contesto di persistenza è chiuso .

Possiamo risolvere questo problema modificando la strategia di recupero di @ElementCollection per utilizzare l'approccio desideroso . Tuttavia, il recupero dei dati con entusiasmo non è necessariamente la soluzione migliore , poiché i dati del telefono verranno sempre caricati, indipendentemente dal fatto che ne abbiamo bisogno o meno.

3. Caricamento dei dati con JPA Query Language

Il linguaggio di query JPA ci consente di personalizzare le informazioni proiettate. Pertanto, possiamo definire un nuovo metodo nel nostro EmployeeRepository per selezionare il dipendente ei suoi telefoni:

public Employee findByJPQL(int id) { return em.createQuery("SELECT u FROM Employee AS u JOIN FETCH u.phones WHERE u.id=:id", Employee.class) .setParameter("id", id).getSingleResult(); } 

La query precedente utilizza un'operazione di inner join per recuperare l'elenco telefonico di ogni dipendente restituito.

4. Caricamento dei dati con il grafico delle entità

Un'altra possibile soluzione è utilizzare la funzione del grafico delle entità da JPA. Il grafico delle entità ci consente di scegliere quali campi verranno proiettati dalle query JPA. Definiamo un altro metodo nel nostro repository:

public Employee findByEntityGraph(int id) { EntityGraph entityGraph = em.createEntityGraph(Employee.class); entityGraph.addAttributeNodes("name", "phones"); Map properties = new HashMap(); properties.put("javax.persistence.fetchgraph", entityGraph); return em.find(Employee.class, id, properties); } 

Possiamo vedere che il nostro grafico delle entità include due attributi: nome e telefoni . Quindi, quando JPA lo traduce in SQL, proietterà le colonne correlate.

5. Caricamento dei dati in un ambito transazionale

Infine, esploreremo un'ultima soluzione. Finora, abbiamo visto che il problema è correlato al ciclo di vita del Contesto di Persistenza.

Quello che succede è che il nostro contesto di persistenza è nell'ambito della transazione e rimarrà aperto fino al termine della transazione . Il ciclo di vita della transazione va dall'inizio alla fine dell'esecuzione del metodo di repository.

Quindi, creiamo un altro test case e configuriamo il nostro contesto di persistenza per eseguire il binding a una transazione avviata dal nostro metodo di test. Terremo aperto il contesto di persistenza fino al termine del test:

@Test @Transactional public void whenUseTransaction_thenFetchResult() { Employee employee = employeeRepository.findById(1); assertThat(employee.getPhones().size(), is(2)); } 

L' annotazione @Transactional configura un proxy transazionale attorno all'istanza della relativa classe di test. Inoltre, la transazione è associata al thread che la esegue. Considerando l'impostazione di propagazione della transazione predefinita, ogni contesto di persistenza creato da questo metodo si unisce a questa stessa transazione. Di conseguenza, il contesto di persistenza della transazione è vincolato all'ambito della transazione del metodo di test.

6. Conclusione

In questo tutorial, abbiamo valutato tre diverse soluzioni per affrontare il problema della lettura dei dati da associazioni pigre in un contesto di persistenza chiuso .

Innanzitutto, abbiamo utilizzato il linguaggio di query JPA per recuperare le raccolte di elementi. Successivamente, abbiamo definito un grafico di entità per recuperare i dati necessari.

E, nella soluzione definitiva, abbiamo utilizzato la transazione Spring per mantenere aperto il contesto di persistenza e leggere i dati necessari.

Come sempre, il codice di esempio per questo tutorial è disponibile su GitHub.

Fondo Java

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO