Guide rapide de la propriété Hibernate enable_lazy_load_no_trans

1. Vue d'ensemble

Lors de l'utilisation du chargement paresseux dans Hibernate, nous pourrions faire face à des exceptions, disant qu'il n'y a pas de session.

Dans ce didacticiel, nous verrons comment résoudre ces problèmes de chargement différé. Pour ce faire, nous utiliserons Spring Boot pour explorer un exemple.

2. Problèmes de chargement différé

Le but du chargement différé est d'économiser des ressources en ne chargeant pas les objets associés en mémoire lorsque nous chargeons l'objet principal. Au lieu de cela, nous reportons l'initialisation des entités paresseuses jusqu'au moment où elles sont nécessaires. Hibernate utilise des proxies et des wrappers de collection pour implémenter le chargement différé.

Lors de la récupération de données chargées paresseusement, le processus se déroule en deux étapes. Premièrement, il y a le remplissage de l'objet principal, et deuxièmement, la récupération des données dans ses proxys. Le chargement des données nécessite toujours une session ouverte dans Hibernate.

Le problème survient lorsque la deuxième étape se produit après la clôture de la transaction , ce qui conduit à une exception LazyInitializationException .

L'approche recommandée consiste à concevoir notre application de manière à garantir que la récupération des données s'effectue en une seule transaction. Mais cela peut parfois être difficile lors de l'utilisation d'une entité paresseuse dans une autre partie du code qui est incapable de déterminer ce qui a été chargé ou non.

Hibernate a une solution de contournement, une propriété enable_lazy_load_no_trans . L'activation de cette option signifie que chaque extraction d'une entité paresseuse ouvrira une session temporaire et s'exécutera dans une transaction distincte.

3. Exemple de chargement différé

Examinons le comportement du chargement différé dans quelques scénarios.

3.1 Configurer les entités et les services

Supposons que nous ayons deux entités, User et Document . Un utilisateur peut avoir plusieurs documents , et nous utiliserons @OneToMany pour décrire cette relation. De plus, nous utiliserons @Fetch (FetchMode.SUBSELECT) pour plus d'efficacité.

Nous devons noter que, par défaut, @OneToMany a un type de récupération paresseuse.

Définissons maintenant notre entité utilisateur :

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

Ensuite, nous avons besoin d'une couche de service avec deux méthodes pour illustrer les différentes options. L'un d'eux est annoté comme @Transactional . Ici, les deux méthodes exécutent la même logique en comptant tous les documents de tous les utilisateurs:

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

Examinons maintenant de plus près les trois exemples suivants. Nous utiliserons également SQLStatementCountValidator pour comprendre l'efficacité de la solution, en comptant le nombre de requêtes exécutées.

3.2. Chargement paresseux avec une transaction environnante

Tout d'abord, utilisons le chargement différé de la manière recommandée. Nous appellerons donc notre méthode @Transactional dans la couche de service:

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

Comme nous pouvons le voir, cela fonctionne et se traduit par deux allers-retours vers la base de données . Le premier aller-retour sélectionne les utilisateurs et le second sélectionne leurs documents.

3.3. Chargement paresseux en dehors d'une transaction

Maintenant, appelons une méthode non transactionnelle pour simuler l'erreur que nous obtenons sans transaction environnante:

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

Comme prévu, cela entraîne une erreur car la fonction getDocs de User est utilisée en dehors d'une transaction.

3.4. Chargement paresseux avec transaction automatique

Pour résoudre ce problème, nous pouvons activer la propriété:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

Avec la propriété activée, nous n'obtenons plus d' exception LazyInitializationException .

Cependant, le décompte des requêtes montre que six allers-retours ont été effectués vers la base de données . Ici, un aller-retour sélectionne les utilisateurs et cinq allers-retours sélectionnent des documents pour chacun des cinq utilisateurs:

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

Nous avons rencontré le fameux problème N + 1 , malgré le fait que nous ayons défini une stratégie de récupération pour l'éviter!

4. Comparaison des approches

Discutons brièvement des avantages et des inconvénients.

Avec la propriété activée, nous n'avons pas à nous soucier des transactions et de leurs limites. Hibernate gère cela pour nous.

Cependant, la solution fonctionne lentement, car Hibernate démarre une transaction pour nous à chaque extraction.

Cela fonctionne parfaitement pour les démos et lorsque nous ne nous soucions pas des problèmes de performances. Cela peut être correct s'il est utilisé pour récupérer une collection qui ne contient qu'un seul élément ou un seul objet lié dans une relation un à un.

Sans la propriété, nous avons un contrôle précis des transactions et nous ne sommes plus confrontés à des problèmes de performance.

Dans l'ensemble, ce n'est pas une fonctionnalité prête pour la production , et la documentation Hibernate nous avertit:

Bien que l'activation de cette configuration puisse faire disparaître LazyInitializationException , il est préférable d'utiliser un plan de récupération qui garantit que toutes les propriétés sont correctement initialisées avant la fermeture de la session.

5. Conclusion

Dans ce didacticiel, nous avons exploré la gestion du chargement différé.

Nous avons essayé une propriété Hibernate pour aider à surmonter l' exception LazyInitializationException . Nous avons également vu comment cela réduit l'efficacité et ne peut être une solution viable que pour un nombre limité de cas d'utilisation.

Comme toujours, tous les exemples de code sont disponibles à l'adresse over sur GitHub.