Je comprends que le problème N + 1 se pose lorsqu'une requête est exécutée pour extraire N enregistrements et N requêtes pour extraire des enregistrements relationnels.
Mais comment peut-on l'éviter à Hibernate?
Supposons que nous ayons un fabricant de classe ayant une relation plusieurs à un avec Contact.
Nous résolvons ce problème en veillant à ce que la requête initiale récupère toutes les données nécessaires au chargement des objets dont nous avons besoin dans leur état initialisé approprié. Une façon de faire consiste à utiliser une jointure de récupération HQL. Nous utilisons le HQL
"from Manufacturer manufacturer join fetch manufacturer.contact contact"
avec l'instruction fetch. Cela se traduit par une jointure interne:
select MANUFACTURER.id from manufacturer and contact ... from
MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id
En utilisant une requête de critères, nous pouvons obtenir le même résultat de
Criteria criteria = session.createCriteria(Manufacturer.class);
criteria.setFetchMode("contact", FetchMode.EAGER);
qui crée le SQL:
select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on
MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1
dans les deux cas, notre requête renvoie une liste d'objets Fabricant avec le contact initialisé. Une seule requête doit être exécutée pour renvoyer toutes les informations de contact et de fabricant requises
pour plus d'informations, voici un lien vers le problem et le solution
Le problème de requête N + 1 se produit lorsque vous oubliez de récupérer une association et que vous devez ensuite y accéder.
Par exemple, supposons que nous ayons la requête JPA suivante:
List<PostComment> comments = entityManager.createQuery(
"select pc " +
"from PostComment pc " +
"where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
Maintenant, si nous itérons les entités PostComment
et parcourons l'association post
:
for(PostComment comment : comments) {
LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}
Hibernate générera les instructions SQL suivantes:
SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM post_comment pc
WHERE pc.review = 'Excellent!'
INFO - Loaded 3 comments
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 1
INFO - The post title is 'Post nr. 1'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 2
INFO - The post title is 'Post nr. 2'
SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM post pc
WHERE pc.id = 3
INFO - The post title is 'Post nr. 3'
C'est ainsi que le problème de requête N + 1 est généré.
Puisque l'association post
n'est pas initialisée lors de l'extraction des entités PostComment
, Hibernate doit extraire l'entité Post
avec une requête secondaire, et pour N entités PostComment
, N autres requêtes vont être exécutées (d'où le problème de requête N + 1).
La première chose à faire pour résoudre ce problème consiste à ajouter une journalisation et une surveillance SQL appropriées . Sans la journalisation, vous ne remarquerez pas le problème de requête N + 1 lors du développement d'une certaine fonctionnalité.
Deuxièmement, pour résoudre ce problème, vous pouvez simplement JOIN FETCH la relation à l'origine de ce problème:
List<PostComment> comments = entityManager.createQuery(
"select pc " +
"from PostComment pc " +
"join fetch pc.post p " +
"where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
Si vous devez extraire plusieurs associations enfants, il est préférable d'extraire une collection de la requête initiale et la seconde avec une requête SQL secondaire.
Il est préférable que ce problème soit détecté par des tests d'intégration. Vous pouvez utiliser une assertion automatic JUnit pour valider le nombre attendu d'instructions SQL générées . Le projet db-util fournit déjà cette fonctionnalité. Il est en open source et la dépendance est disponible sur Maven Central.
La solution native pour 1 + N dans Hibernate s’appelle:
À l'aide de la récupération par lots, Hibernate peut charger plusieurs proxies non initialisés si un proxy est utilisé. L'extraction par lots est une optimisation de la stratégie d'extraction sélective paresseuse. Il existe deux manières de configurer l'extraction par lots: au niveau 1) de la classe et au niveau 2 de la collection ...
Vérifiez ces questions et réponses:
Avec les annotations, nous pouvons le faire comme ceci:
Un niveau class
:
@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements Java.io.Serializable {...
Un niveau collection
:
@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl()
Le chargement différé et le chargement par lots représentent une optimisation qui:
Vous pouvez même le faire fonctionner sans avoir à ajouter l'annotation @BatchSize
partout; il vous suffit de définir la propriété hibernate.default_batch_fetch_size
sur la valeur souhaitée pour activer la récupération par lots de manière globale. Voir le Hibernate docs pour plus de détails.
Pendant que vous y êtes, vous voudrez probablement aussi changer le BatchFetchStyle , car la valeur par défaut (LEGACY
) n’est probablement pas celle que vous voulez. Ainsi, une configuration complète permettant globalement l'extraction par lots ressemblerait à ceci:
hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25
En outre, je suis surpris que l'une des solutions proposées implique la récupération de jointure. La récupération de jointure est rarement souhaitable, car elle entraîne le transfert de plus de données avec chaque ligne de résultat, même si l'entité dépendante a déjà été chargée dans le cache L1 ou L2. Ainsi, je recommanderais de le désactiver complètement en réglant
hibernate.max_fetch_depth=0