web-dev-qa-db-fra.com

Quelle est la solution au problème N + 1 dans JPA et Hibernate?

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?

22
Vipul Agarwal

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

26
karim mohsen

Le problème

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).

Le correctif

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. 

13
Vlad Mihalcea

La solution native pour 1 + N dans Hibernate s’appelle:

20.1.5. Utilisation de la récupération par lot

À 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:

  • est-ce que non requiert aucun extraction explicite dans nos requêtes
  • sera appliqué à n'importe quel nombre de références} _ qui sont (paresseusement) touchées après le chargement de l'entité racine (alors que l'extraction explicite n'affecte que ceux nommés dans la requête)
  • résoudra le problème 1 + N avec collections(car une seule collection pourrait être récupérée avec la requête racine) sans avoir besoin de traitement supplémentaire Pour obtenir les valeurs racine DISTINCT (cocher : Critères.DISTINCT_ROOT_ENTITY vs Projections.distinct )
12
Radim Köhler

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
0
philippn