Utilisation des collections d'éléments paresseux dans JPA

Haut Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

La spécification JPA propose deux stratégies d'extraction différentes: impatiente et paresseuse. Bien que l'approche paresseuse aide à éviter de charger inutilement des données dont nous n'avons pas besoin, nous avons parfois besoin de lire des données non initialement chargées dans un contexte de persistance fermé. De plus, l'accès aux collections d'éléments paresseux dans un contexte de persistance fermé est un problème courant.

Dans ce didacticiel, nous allons nous concentrer sur la façon de charger des données à partir de collections d'éléments différés. Nous explorerons trois solutions différentes: l'une impliquant le langage de requête JPA, une autre avec l'utilisation de graphes d'entités et la dernière avec la propagation des transactions.

2. Le problème de la collecte des éléments

Par défaut, JPA utilise la stratégie de récupération différée dans les associations de type @ElementCollection . Ainsi, tout accès à la collection dans un contexte de persistance fermé entraînera une exception.

Pour comprendre le problème, définissons un modèle de domaine basé sur la relation entre l'employé et sa liste téléphonique:

@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 }

Notre modèle précise qu'un employé peut avoir plusieurs téléphones. La liste de téléphones est une collection de types intégrables . Utilisons un référentiel Spring avec ce modèle:

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

Maintenant, reproduisons le problème avec un cas de test JUnit simple:

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)); } } 

Ce test lève une exception lorsque nous essayons d'accéder à la liste téléphonique car le contexte de persistance est fermé .

Nous pouvons résoudre ce problème en modifiant la stratégie de récupération de @ElementCollection pour utiliser l'approche hâtive . Cependant, récupérer les données avec empressement n'est pas nécessairement la meilleure solution , car les données du téléphone seront toujours chargées, que nous en ayons besoin ou non.

3. Chargement des données avec le langage de requête JPA

Le langage de requête JPA nous permet de personnaliser les informations projetées. Par conséquent, nous pouvons définir une nouvelle méthode dans notre EmployeeRepository pour sélectionner l'employé et ses téléphones:

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 requête ci-dessus utilise une opération de jointure interne pour récupérer la liste téléphonique de chaque employé renvoyé.

4. Chargement des données avec le graphique d'entité

Une autre solution possible consiste à utiliser la fonctionnalité de graphe d'entités de JPA. Le graphe d'entités nous permet de choisir les champs qui seront projetés par les requêtes JPA. Définissons une autre méthode dans notre référentiel:

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); } 

Nous pouvons voir que notre graphique d'entité comprend deux attributs: le nom et les téléphones . Ainsi, lorsque JPA traduit cela en SQL, il projettera les colonnes associées.

5. Chargement de données dans une étendue transactionnelle

Enfin, nous allons explorer une dernière solution. Jusqu'à présent, nous avons vu que le problème est lié au cycle de vie du contexte de persistance.

Ce qui se passe, c'est que notre contexte de persistance a une portée transactionnelle et restera ouvert jusqu'à la fin de la transaction . Le cycle de vie de la transaction s'étend du début à la fin de l'exécution de la méthode de référentiel.

Alors, créons un autre cas de test et configurons notre contexte de persistance pour se lier à une transaction démarrée par notre méthode de test. Nous garderons le contexte de persistance ouvert jusqu'à la fin du test:

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

L' annotation @Transactional configure un proxy transactionnel autour de l'instance de la classe de test associée. De plus, la transaction est associée au thread qui l'exécute. Compte tenu du paramètre de propagation de transaction par défaut, chaque contexte de persistance créé à partir de cette méthode se joint à cette même transaction. Par conséquent, le contexte de persistance de transaction est lié à la portée de transaction de la méthode de test.

6. Conclusion

Dans ce didacticiel, nous avons évalué trois solutions différentes pour résoudre le problème de la lecture de données à partir d'associations paresseuses dans un contexte de persistance fermé .

Tout d'abord, nous avons utilisé le langage de requête JPA pour récupérer les collections d'éléments. Ensuite, nous avons défini un graphe d'entités pour récupérer les données nécessaires.

Et, dans la solution ultime, nous avons utilisé la transaction Spring pour garder le contexte de persistance ouvert et lire les données nécessaires.

Comme toujours, l'exemple de code de ce didacticiel est disponible à l'adresse over sur GitHub.

Fond Java

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS