Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 46

Optimisation JPA: Résoudre les requêtes N+1

Recommended resources

Bonjour !

Dans la leçon précédente, nous avons appris à jouer les détectives en utilisant Spring Boot Actuator et le profileur VisualVM pour identifier les goulots d'étranglement dans notre application. Nous savons maintenant comment repérer un endpoint qui est anormalement lent.

Aujourd'hui, nous allons nous attaquer à l'un des coupables les plus fréquents de ces lenteurs : le problème de requêtes N+1. C'est un piège de performance classique dans les applications qui utilisent un ORM comme JPA, et savoir le diagnostiquer et le corriger est une compétence essentielle pour tout développeur backend senior.

L'objectif de cette leçon est de vous rendre capable d'identifier et de résoudre le problème des requêtes N+1 dans JPA. Nous verrons ce que c'est, comment le repérer dans les logs, et surtout, comment l'éradiquer avec des techniques professionnelles.

Database Performance: The N+1 Select Problem
Ce diagramme illustre parfaitement l'impact du problème N+1. La ligne rouge montre comment le temps de réponse explose lorsque le nombre de requêtes augmente (N+1), tandis que la ligne verte montre la performance stable d'une requête optimisée. Notre but aujourd'hui est de transformer les lignes rouges en lignes vertes.

1. Qu'est-ce que le problème N+1 ?

Si vous avez déjà utilisé un ORM comme Eloquent dans Laravel, vous savez combien il est pratique de naviguer dans les relations entre objets, par exemple product.getReviews(). Cependant, cette abstraction peut cacher une inefficacité redoutable.

Le problème N+1 se produit lorsque vous chargez une liste d'entités (la requête 1), puis que vous parcourez cette liste en accédant à une relation "lazy" (chargée paresseusement) pour chaque entité, déclenchant ainsi N requêtes supplémentaires, une pour chaque entité de la liste.

Pour mieux comprendre ce concept et le voir en action, nous allons regarder une vidéo très claire qui illustre le problème avec un exemple concret.

Interview Q&A | Hibernate N+1 Problem & Fix - The Bug You Didn’t Know You Had @Javatechie

La chaîne Java Techie propose une excellente démonstration du problème N+1. La première partie explique le concept, et la seconde le met en évidence dans une application Spring Boot.

Regardez les deux parties suivantes de la vidéo : Explication du concept (00:29 - 05:00) : Concentrez-vous sur l'exemple Customer et Address pour bien comprendre pourquoi on parle de "N+1". Démonstration pratique (05:00 - 09:00) : Observez comment une simple requête findAll() se traduit par une avalanche de requêtes SQL dans la console. C'est la manifestation concrète du problème.


2. Comment identifier le problème ?

La première étape pour résoudre un problème est de savoir qu'il existe. Comme nous l'avons vu dans la leçon précédente, un temps de réponse élevé sur un endpoint Actuator est un premier indice. La confirmation viendra des logs de votre application.

L'article suivant, très complet et en français, vous guidera sur les étapes pratiques pour rendre ce problème visible.

Spring Data JPA : performances des N+1 queries et comment les ...

Cet article de Bienvenus Tizi sur LinkedIn est une mine d'or sur le sujet. Nous allons commencer par lire les sections qui expliquent comment mettre le problème en évidence.

Lisez la section intitulée "Comment J'ai Découvert le Problème (et Comment Vous Pouvez l'Éviter)". Concentrez-vous sur deux points : Activer les Logs SQL : La configuration dans application.properties est essentielle pour voir les requêtes générées par JPA. Comprendre le Fetch Type : Saisissez la différence entre FetchType.LAZY (par défaut pour les collections) et FetchType.EAGER. Comprenez pourquoi LAZY est la source du problème N+1 s'il n'est pas géré, et pourquoi EAGER n'est souvent pas une bonne solution (il charge systématiquement les données, même si inutiles).

En résumé, la stratégie de détection est :

  1. Suspecter : un endpoint listant des entités avec leurs relations est lent.
  2. Confirmer : activer les logs SQL et observer un schéma de requêtes répétitif : une requête pour la liste principale, suivie de nombreuses requêtes pour charger les relations.

3. Comment résoudre le problème ?

Maintenant que nous savons identifier le problème, voyons comment le corriger. Il existe plusieurs stratégies, mais nous allons nous concentrer sur les deux plus efficaces et les plus utilisées en milieu professionnel : JOIN FETCH et @EntityGraph.

Solution 1 : JOIN FETCH avec @Query

C'est la solution la plus directe. Vous écrivez vous-même la requête JPQL en spécifiant explicitement à JPA de charger les relations nécessaires en une seule fois grâce à une jointure.

Reprenons l'article LinkedIn et regardons comment mettre en œuvre cette solution.

Spring Data JPA : performances des N+1 queries et comment les ...

L'article explique très bien comment utiliser JOIN FETCH.

Lisez la section "Solution 1 : JOIN FETCH - Le Bouclier Anti-N+1". Notez la syntaxe de la requête avec @Query et le mot-clé FETCH. L'article mentionne aussi un piège important : le produit cartésien lors de JOIN FETCH multiples, qui peut consommer beaucoup de mémoire. C'est un point crucial à retenir.

Pour voir cette solution en action, nous allons regarder la fin de la vidéo de Java Techie.

Interview Q&A | Hibernate N+1 Problem & Fix - The Bug You Didn’t Know You Had @Javatechie

Cette partie de la vidéo montre concrètement comment implémenter une requête personnalisée avec JOIN FETCH et le résultat immédiat sur le nombre de requêtes SQL.

Regardez de 11:28 à 13:54. Observez comment le LEFT JOIN FETCH dans la requête @Query transforme les N+1 requêtes en une seule requête LEFT JOIN optimisée.

Solution 2 : @EntityGraph

@EntityGraph est une approche plus déclarative et souvent plus propre. Au lieu d'écrire la jointure dans la requête, vous déclarez quelles relations (attributePaths) doivent être chargées avec l'entité. Spring Data JPA se charge ensuite de générer la requête optimisée.

Spring Data JPA : performances des N+1 queries et comment les ...

L'article présente @EntityGraph comme une alternative élégante.

Lisez maintenant la section "Solution 2 : @EntityGraph - La Déclaration Élégante". Comprenez comment utiliser l'annotation @EntityGraph directement sur une méthode du repository pour spécifier les relations à charger.

Voyons également la démonstration vidéo de cette approche.

Interview Q&A | Hibernate N+1 Problem & Fix - The Bug You Didn’t Know You Had @Javatechie

Java Techie montre aussi comment utiliser @EntityGraph pour résoudre le problème. C'est une excellente comparaison avec l'approche JOIN FETCH.

Regardez de 09:00 à 11:28. Vous verrez comment l'ajout de l'annotation @EntityGraph(attributePaths = {"addresses"}) sur la méthode findAll() suffit à résoudre le problème.

Quelle solution choisir ?

  • @Query avec JOIN FETCH : Offre un contrôle total. Idéal pour des requêtes complexes, avec des WHERE spécifiques sur les entités jointes.
  • @EntityGraph : Plus propre, plus lisible et réutilisable, surtout si la même "vue" d'une entité est nécessaire à plusieurs endroits. Parfait pour les cas d'usage standards.

Une autre approche : @BatchSize

Il existe une solution de compromis, utile lorsque charger toutes les relations d'un coup consommerait trop de mémoire. @BatchSize permet de charger les collections par lots. Au lieu de N requêtes, JPA en fera environ N / batch_size.

L'article LinkedIn (LINK) l'explique dans sa section "Solution 4 : @BatchSize - Le Compromis Intelligent". C'est une bonne technique à garder en tête pour les cas où vous manipulez de très grands volumes de données.


Synthèse et bonnes pratiques

La clé n'est pas seulement de connaître les solutions, mais de développer le bon état d'esprit. Comme le dit l'auteur de l'article sur LinkedIn, il faut "penser en deux couches" : la couche objet en Java, et la couche relationnelle en SQL. Chaque appel à une méthode de repository ou à un getter sur une relation peut potentiellement se traduire par une requête à la base de données.

Pour vous aider à systématiser cette approche, voici une excellente checklist.

Spring Data JPA : performances des N+1 queries et comment les ...

Pour conclure, l'article propose une checklist très pratique à suivre avant de déployer du code utilisant JPA.

Lisez attentivement la section "Votre Checklist Anti-N+1". Essayez d'intégrer ces vérifications dans votre routine de développement.


Conclusion

Félicitations ! Vous avez maintenant les outils pour traquer et éliminer l'un des problèmes de performance les plus courants dans les applications web.

Voici les points clés à retenir :

  • Le problème N+1 est causé par l'accès itératif à des relations LAZY après avoir chargé une liste d'entités, résultant en une requête initiale + N requêtes supplémentaires.
  • Il est visible dans les logs SQL par une série de requêtes SELECT identiques qui se répètent.
  • La solution principale est de forcer le chargement des données en une seule fois via une requête optimisée.
  • Vous pouvez y parvenir de manière impérative avec @Query et JOIN FETCH ou de manière déclarative avec @EntityGraph.

Dans la leçon précédente, vous avez appris à identifier qu'un endpoint est lent. Aujourd'hui, vous avez appris à corriger une des raisons majeures : un nombre excessif de requêtes.

Prochaine étape :
Maintenant que nous savons comment réduire le nombre de requêtes, il est temps de nous intéresser à la performance de chaque requête individuelle. Dans la prochaine leçon, nous apprendrons à analyser un plan d'exécution SQL pour optimiser les requêtes de base de données. Vous découvrirez comment la base de données exécute réellement vos requêtes et comment l'aider à être plus efficace, notamment grâce aux index.