La récupération impatiente JPA ne rejoint pas


112

Que contrôle exactement la stratégie de récupération de JPA? Je ne peux détecter aucune différence entre impatient et paresseux. Dans les deux cas, JPA / Hibernate ne joint pas automatiquement les relations plusieurs-à-un.

Exemple: la personne a une seule adresse. Une adresse peut appartenir à plusieurs personnes. Les classes d'entités annotées JPA ressemblent à:

@Entity
public class Person {
    @Id
    public Integer id;

    public String name;

    @ManyToOne(fetch=FetchType.LAZY or EAGER)
    public Address address;
}

@Entity
public class Address {
    @Id
    public Integer id;

    public String name;
}

Si j'utilise la requête JPA:

select p from Person p where ...

JPA / Hibernate génère une requête SQL à sélectionner dans la table Person, puis une requête d'adresse distincte pour chaque personne:

select ... from Person where ...
select ... from Address where id=1
select ... from Address where id=2
select ... from Address where id=3

C'est très mauvais pour les grands ensembles de résultats. S'il y a 1000 personnes, il génère 1001 requêtes (1 de Person et 1000 distinctes de Address). Je le sais parce que je regarde le journal des requêtes de MySQL. J'ai cru comprendre que la définition du type de récupération d'adresse sur impatient entraînera JPA / Hibernate à interroger automatiquement avec une jointure. Cependant, quel que soit le type d'extraction, il génère toujours des requêtes distinctes pour les relations.

Ce n'est que lorsque je lui dis explicitement de se joindre qu'il rejoint réellement:

select p, a from Person p left join p.address a where ...

Est-ce que j'ai râté quelque chose? Je dois maintenant coder chaque requête pour qu'elle rejoigne les relations plusieurs-à-un. J'utilise l'implémentation JPA d'Hibernate avec MySQL.

Edit: Il apparaît (voir FAQ Hibernate ici et ici ) qui FetchTypen'a pas d'impact sur les requêtes JPA. Donc, dans mon cas, je lui ai explicitement dit de se joindre.


5
Les liens vers les entrées de la FAQ sont rompus, en voici un qui
n0weak

Réponses:


99

JPA ne fournit aucune spécification sur les annotations de mappage pour sélectionner la stratégie de récupération. En général, les entités associées peuvent être récupérées de l'une des manières indiquées ci-dessous

  • SELECT => une requête pour les entités racine + une requête pour l'entité mappée associée / collection de chaque entité racine = (n + 1) requêtes
  • SUBSELECT => une requête pour les entités racine + deuxième requête pour l'entité mappée associée / collection de toutes les entités racine récupérées dans la première requête = 2 requêtes
  • JOIN => une requête pour récupérer les deux entités racine et toute leur entité / collection mappée = 1 requête

Donc SELECTet JOINsont deux extrêmes et se SUBSELECTsitue entre les deux. On peut choisir une stratégie appropriée en fonction de son modèle de domaine.

Par défaut, il SELECTest utilisé à la fois par JPA / EclipseLink et Hibernate. Cela peut être remplacé en utilisant:

@Fetch(FetchMode.JOIN) 
@Fetch(FetchMode.SUBSELECT)

dans Hibernate. Il permet également de définir le SELECTmode explicitement en utilisant @Fetch(FetchMode.SELECT)ce qui peut être réglé en utilisant la taille du lot, par exemple @BatchSize(size=10).

Les annotations correspondantes dans EclipseLink sont:

@JoinFetch
@BatchFetch

5
Pourquoi ces paramètres existent-ils? Je pense que JOIN doit être utilisé presque toujours. Maintenant, je dois marquer tous les mappages avec des annotations spécifiques à la mise en veille prolongée.
vbezhenar

4
Intéressant mais malheureusement @Fetch (FetchMode.JOIN) ne fonctionne pas du tout pour moi (Hibernate 4.2.15), en JPQL comme dans Criteria.
Aphax

3
L'annotation Hibernate ne semble pas non plus fonctionner du tout pour moi, en utilisant Spring JPA
TheBakker

2
@Aphax Cela peut être dû au fait qu'Hibernate utilise différentes stratégies par défaut pour JPAQL / Criteria et em.find (). Voir vladmihalcea.com/2013/10/17/… et la documentation de référence.
Joshua Davis

1
@vbezhenar (et d'autres lisant son commentaire un peu plus tard): La requête JOIN génère un produit cartésien dans la base de données, vous devez donc être sûr que vous voulez que ce produit cartésien soit calculé. Notez que si vous utilisez la jointure fetch, même si vous mettez LAZY, elle sera chargée avec impatience.
Walfrat

45

"mxc" a raison. fetchTypespécifie simplement quand la relation doit être résolue.

Pour optimiser le chargement hâtif en utilisant une jointure externe, vous devez ajouter

@Fetch(FetchMode.JOIN)

à votre domaine. Il s'agit d'une annotation spécifique à l'hibernation.


6
Cela ne fonctionne pas pour moi avec Hibernate 4.2.15, dans JPQL ou Criteria.
Aphax

6
@Aphax Je pense que c'est parce que JPAQL et Criteria n'obéissent pas à la spécification Fetch. L'annotation Fetch ne fonctionne que pour em.find (), AFAIK. Voir vladmihalcea.com/2013/10/17/… Voir également les docos hibernate. Je suis presque sûr que cela est couvert quelque part.
Joshua Davis

@JoshuaDavis Ce que je veux dire, c'est que l'annotation \ @Fetch n'applique aucune sorte d'optimisation JOIN dans les requêtes, que ce soit JPQL ou em.find (), je viens de faire un autre essai sur Hibernate 5.2. + Et c'est toujours la même chose
Aphax

38

L'attribut fetchType contrôle si le champ annoté est extrait immédiatement lorsque l'entité principale est extraite. Cela ne dicte pas nécessairement la manière dont l'instruction fetch est construite, l'implémentation réelle de sql dépend du fournisseur que vous utilisez toplink / hibernate, etc.

Si vous définissez fetchType=EAGERCela signifie que le champ annoté est rempli avec ses valeurs en même temps que les autres champs de l'entité. Ainsi, si vous ouvrez un entitymanager récupérez vos objets person, puis fermez le entitymanager, le fait de faire par la suite un person.address n'entraînera pas la levée d'une exception de chargement différé.

Si vous définissez, fetchType=LAZYle champ n'est renseigné que lors de l'accès. Si vous avez fermé le gestionnaire d'entités d'ici là, une exception de chargement différé sera levée si vous faites une personne.adresse. Pour charger le champ, vous devez remettre l'entité dans un contexte entitymangers avec em.merge (), puis effectuez l'accès au champ, puis fermez le gestionnaire d'entités.

Vous pouvez souhaiter un chargement différé lors de la construction d'une classe de clients avec une collection pour les commandes des clients. Si vous avez récupéré chaque commande d'un client lorsque vous vouliez obtenir une liste de clients, cela peut être une opération de base de données coûteuse lorsque vous recherchez uniquement le nom du client et les coordonnées. Il est préférable de laisser l'accès à la base de données plus tard.

Pour la deuxième partie de la question - comment mettre en veille prolongée pour générer un SQL optimisé?

Hibernate devrait vous permettre de fournir des conseils sur la façon de construire la requête la plus efficace, mais je soupçonne qu'il y a un problème avec la construction de votre table. La relation est-elle établie dans les tableaux? Hibernate a peut-être décidé qu'une simple requête serait plus rapide qu'une jointure, surtout s'il manque des index, etc.


20

Essayez avec:

select p from Person p left join FETCH p.address a where...

Cela fonctionne pour moi de manière similaire avec JPA2 / EclipseLink, mais il semble que cette fonctionnalité soit également présente dans JPA1 :



2

pour rejoindre, vous pouvez faire plusieurs choses (en utilisant eclipselink)

  • en jpql, vous pouvez effectuer une récupération de jointure gauche

  • dans la requête nommée, vous pouvez spécifier une indication de requête

  • dans TypedQuery, vous pouvez dire quelque chose comme

    query.setHint("eclipselink.join-fetch", "e.projects.milestones");

  • il y a aussi un indice de récupération par lots

    query.setHint("eclipselink.batch", "e.address");

voir

http://java-persistence-performance.blogspot.com/2010/08/batch-fetching-optimizing-object-graph.html


1

J'ai eu exactement ce problème à l'exception du fait que la classe Person avait une classe de clé intégrée. Ma propre solution était de les rejoindre dans la requête ET de supprimer

@Fetch(FetchMode.JOIN)

Ma classe d'identifiant intégrée:

@Embeddable
public class MessageRecipientId implements Serializable {

    @ManyToOne(targetEntity = Message.class, fetch = FetchType.LAZY)
    @JoinColumn(name="messageId")
    private Message message;
    private String governmentId;

    public MessageRecipientId() {
    }

    public Message getMessage() {
        return message;
    }

    public void setMessage(Message message) {
        this.message = message;
    }

    public String getGovernmentId() {
        return governmentId;
    }

    public void setGovernmentId(String governmentId) {
        this.governmentId = governmentId;
    }

    public MessageRecipientId(Message message, GovernmentId governmentId) {
        this.message = message;
        this.governmentId = governmentId.getValue();
    }

}

-1

Deux choses me viennent à l'esprit.

Tout d'abord, êtes-vous sûr de vouloir dire ManyToOne pour adresse? Cela signifie que plusieurs personnes auront la même adresse. S'il est modifié pour l'un d'entre eux, il sera modifié pour tous. Est-ce votre intention? 99% du temps, les adresses sont «privées» (en ce sens qu'elles n'appartiennent qu'à une seule personne).

Deuxièmement, avez-vous d'autres relations enthousiastes sur l'entité Personne? Si je me souviens bien, Hibernate ne peut gérer qu'une seule relation enthousiaste sur une entité, mais ce sont peut-être des informations obsolètes.

Je dis cela parce que votre compréhension de la façon dont cela devrait fonctionner est essentiellement correcte à partir de là où je suis assis.


Ceci est un exemple inventé qui utilise plusieurs-à-un. L'adresse-personne n'est peut-être pas le meilleur exemple. Je ne vois aucun autre type de récupération impatiente dans mon code.
Steve Kuo

Ma suggestion est alors de réduire cela à un exemple simple qui s'exécute et fait ce que vous voyez, puis de le publier. Il peut y avoir d'autres complications dans votre modèle qui provoquent un comportement inattendu.
cletus

1
J'ai exécuté le code exactement comme il apparaît ci-dessus et il présente ledit comportement.
Steve Kuo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.