Comment puis-je paresser une relation JPA OneToOne

212

Dans cette application que nous développons, nous avons remarqué qu'une vue était particulièrement lente. J'ai profilé la vue et j'ai remarqué qu'il y avait une requête exécutée par hibernate qui prenait 10 secondes même s'il n'y avait que deux objets dans la base de données à récupérer. Tout OneToManyet les ManyToManyrelations étaient paresseux donc ce n'était pas le problème. Lors de l'inspection du SQL en cours d'exécution, j'ai remarqué qu'il y avait plus de 80 jointures dans la requête.

En inspectant davantage le problème, j'ai remarqué que le problème était dû à la hiérarchie profonde OneToOneet aux ManyToOnerelations entre les classes d'entités. Donc, j'ai pensé, je vais juste les rendre paresseux, cela devrait résoudre le problème. Mais l'annotation soit @OneToOne(fetch=FetchType.LAZY)ou @ManyToOne(fetch=FetchType.LAZY)ne semble pas fonctionner. Soit j'obtiens une exception, soit ils ne sont pas réellement remplacés par un objet proxy et sont donc paresseux.

Des idées sur la façon dont je vais faire fonctionner ça? Notez que je n'utilise pas le persistence.xmlpour définir des relations ou des détails de configuration, tout se fait en code java.

Kim L
la source

Réponses:

218

Tout d'abord, quelques clarifications à la réponse de KLE :

  1. L'association un-à-un sans contrainte (nullable) est la seule qui ne peut pas être mandatée sans instrumentation de bytecode. La raison en est que l'entité propriétaire DOIT savoir si la propriété d'association doit contenir un objet proxy ou NULL et elle ne peut pas déterminer cela en regardant les colonnes de sa table de base car un à un est normalement mappé via PK partagé, donc il doit être récupéré avec impatience de toute façon, rendant le proxy inutile. Voici une explication plus détaillée .

  2. les associations plusieurs-à-un (et un-à-plusieurs, évidemment) ne souffrent pas de ce problème. L'entité propriétaire peut facilement vérifier son propre FK (et dans le cas d'un à plusieurs, un proxy de collecte vide est créé initialement et rempli à la demande), de sorte que l'association peut être paresseuse.

  3. Remplacer un à un par un à plusieurs n'est presque jamais une bonne idée. Vous pouvez le remplacer par plusieurs à un, mais il existe d'autres options (peut-être meilleures).

Rob H. a un point valide, mais vous ne pourrez peut-être pas l'implémenter en fonction de votre modèle (par exemple si votre association un à un est annulable).

Maintenant, en ce qui concerne la question d'origine:

A) @ManyToOne(fetch=FetchType.LAZY)devrait très bien fonctionner. Êtes-vous sûr qu'il n'est pas écrasé dans la requête elle-même? Il est possible de spécifier join fetchen HQL et / ou de définir explicitement le mode d'extraction via l'API Criteria qui aurait priorité sur l'annotation de classe. Si ce n'est pas le cas et que vous rencontrez toujours des problèmes, veuillez publier vos classes, votre requête et le SQL résultant pour une conversation plus précise.

B) @OneToOne est plus délicat. Si elle n'est certainement pas annulable, suivez la suggestion de Rob H. et spécifiez-la comme telle:

@OneToOne(optional = false, fetch = FetchType.LAZY)

Sinon, si vous pouvez changer votre base de données (ajoutez une colonne de clé étrangère à la table propriétaire), faites-le et mappez-la comme "jointe":

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name="other_entity_fk")
public OtherEntity getOther()

et dans OtherEntity:

@OneToOne(mappedBy = "other")
public OwnerEntity getOwner()

Si vous ne pouvez pas faire cela (et ne pouvez pas vivre avec une récupération avide), l'instrumentation de bytecode est votre seule option. Je dois cependant être d'accord avec CPerkins - si vous en avez 80 !!! rejoint en raison d'associations OneToOne désireuses, vous avez des problèmes plus importants que celui-ci :-)

ChssPly76
la source
Il y a peut-être une autre option, mais je ne l'ai pas personnellement testée: du côté non contraint, utilisez un one-to-oneavec une formule comme select other_entity.id from other_entity where id = other_entity.id. Bien sûr, ce n'est pas idéal pour les performances des requêtes.
Frédéric
1
facultatif = faux, ne fonctionne pas pour moi. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", facultatif = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts du
21

Pour que le chargement paresseux fonctionne sur des mappages un à un nullables, vous devez laisser hibernate effectuer l’ instrumentation de temps de compilation et ajouter un@LazyToOne(value = LazyToOneOption.NO_PROXY) à la relation un-à-un.

Exemple de mappage:

@OneToOne(fetch = FetchType.LAZY)  
@JoinColumn(name="other_entity_fk")
@LazyToOne(value = LazyToOneOption.NO_PROXY)
public OtherEntity getOther()

Exemple d'extension de fichier Ant Build (pour effectuer l'instrumentation de temps de compilation Hibernate):

<property name="src" value="/your/src/directory"/><!-- path of the source files --> 
<property name="libs" value="/your/libs/directory"/><!-- path of your libraries --> 
<property name="destination" value="/your/build/directory"/><!-- path of your build directory --> 

<fileset id="applibs" dir="${libs}"> 
  <include name="hibernate3.jar" /> 
  <!-- include any other libraries you'll need here --> 
</fileset> 

<target name="compile"> 
  <javac srcdir="${src}" destdir="${destination}" debug="yes"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </javac> 
</target> 

<target name="instrument" depends="compile"> 
  <taskdef name="instrument" classname="org.hibernate.tool.instrument.javassist.InstrumentTask"> 
    <classpath> 
      <fileset refid="applibs"/> 
    </classpath> 
  </taskdef> 

  <instrument verbose="true"> 
    <fileset dir="${destination}"> 
      <!-- substitute the package where you keep your domain objs --> 
      <include name="/com/mycompany/domainobjects/*.class"/> 
    </fileset> 
  </instrument> 
</target>
Kdeveloper
la source
3
Pourquoi LazyToOneOption.NO_PROXYet non LazyToOneOption.PROXY?
Telmo Marques
Cela ne répond pas au «pourquoi», mais ce fait est affirmé ici aussi (vers la fin de la section «Cartographie typique»): vladmihalcea.com/…
DanielM
12

L'idée de base derrière les XToOnes dans Hibernate est qu'ils ne sont pas paresseux dans la plupart des cas.

Une des raisons est que, quand Hibernate doit décider de mettre un proxy (avec l'id) ou un null,
il doit quand même regarder dans l'autre table pour se joindre. Le coût d'accès à l'autre table de la base de données est important, il pourrait donc aussi bien récupérer les données de cette table à ce moment (comportement non paresseux), au lieu de le récupérer dans une demande ultérieure qui nécessiterait un deuxième accès à la même tableau.

Modifié: pour plus de détails, veuillez vous référer à la réponse de ChssPly76 . Celui-ci est moins précis et détaillé, il n'a rien à offrir. Merci ChssPly76.

KLE
la source
Il y a plusieurs choses qui ne vont pas ici - j'ai fourni une autre réponse ci-dessous avec une explication (trop de choses, ne rentrera pas dans un commentaire)
ChssPly76
8

Voici quelque chose qui fonctionne pour moi (sans instrumentation):

Au lieu d'utiliser des @OneToOnedeux côtés, j'utilise @OneToManydans la partie inverse de la relation (celle avec mappedBy). Cela fait de la propriété une collection ( Listdans l'exemple ci-dessous), mais je la traduis en un élément dans le getter, ce qui la rend transparente pour les clients.

Cette configuration fonctionne paresseusement, c'est-à-dire que les sélections ne sont effectuées que lorsque getPrevious()ou getNext()sont appelées - et une seule sélection pour chaque appel.

La structure de la table:

CREATE TABLE `TB_ISSUE` (
    `ID`            INT(9) NOT NULL AUTO_INCREMENT,
    `NAME`          VARCHAR(255) NULL,
    `PREVIOUS`      DECIMAL(9,2) NULL
    CONSTRAINT `PK_ISSUE` PRIMARY KEY (`ID`)
);
ALTER TABLE `TB_ISSUE` ADD CONSTRAINT `FK_ISSUE_ISSUE_PREVIOUS`
                 FOREIGN KEY (`PREVIOUS`) REFERENCES `TB_ISSUE` (`ID`);

La classe:

@Entity
@Table(name = "TB_ISSUE") 
public class Issue {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @Column
    private String name;

    @OneToOne(fetch=FetchType.LAZY)  // one to one, as expected
    @JoinColumn(name="previous")
    private Issue previous;

    // use @OneToMany instead of @OneToOne to "fake" the lazy loading
    @OneToMany(mappedBy="previous", fetch=FetchType.LAZY)
    // notice the type isnt Issue, but a collection (that will have 0 or 1 items)
    private List<Issue> next;

    public Integer getId() { return id; }
    public String getName() { return name; }

    public Issue getPrevious() { return previous; }
    // in the getter, transform the collection into an Issue for the clients
    public Issue getNext() { return next.isEmpty() ? null : next.get(0); }

}
acdcjunior
la source
7

Comme je l'ai expliqué dans cet article , à moins que vous n'utilisiez le Bytecode Enhancement , vous ne pouvez pas récupérer paresseusement l'association parent-côté @OneToOne.

Cependant, le plus souvent, vous n'avez même pas besoin de l'association parent si vous utilisez @MapsIddu côté client:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

Avec @MapsId, la idpropriété de la table enfant sert à la fois de clé primaire et de clé étrangère à la clé primaire de la table parent.

Donc, si vous avez une référence à l' Postentité parent , vous pouvez facilement récupérer l'entité enfant en utilisant l'identifiant d'entité parent:

PostDetails details = entityManager.find(
    PostDetails.class,
    post.getId()
);

De cette façon, vous n'aurez pas de problèmes de requête N + 1 qui pourraient être causés par l' mappedBy @OneToOneassociation du côté parent.

Vlad Mihalcea
la source
de cette façon, nous ne pouvons plus cascader les opérations du parent à l'enfant: /
Hamdi
Pour persist, c'est juste un appel de persistance supplémentaire, pour supprimer, vous pouvez utiliser la cascade DDL.
Vlad Mihalcea
6

Dans les mappages XML Hibernate natifs, vous pouvez accomplir cela en déclarant un mappage un à un avec l' attribut contraint défini sur true. Je ne suis pas sûr de ce que l'équivalent d'annotation Hibernate / JPA est, et une recherche rapide du document n'a fourni aucune réponse, mais j'espère que cela vous donne une piste pour continuer.

Rob H
la source
5
+1 pour une bonne suggestion; malheureusement, il n'est pas toujours applicable car le modèle de domaine peut en fait nécessiter la nullité. La bonne façon de mapper cela via des annotations est@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76
J'ai essayé cela et je n'ai vu aucune amélioration des performances. J'ai encore vu de nombreuses requêtes dans la sortie de mise en veille prolongée via le débogueur.
P.Brian.Mackey
3

Comme déjà parfaitement expliqué par ChssPly76, les mandataires d'Hibernate n'aident pas avec les associations un-à-un sans contrainte (annulables), MAIS il y a une astuce expliquée ici pour éviter de configurer l'instrumentation. L'idée est de tromper Hibernate que la classe d'entité que nous voulons utiliser a déjà été instrumentée: vous l'instruisez manuellement dans le code source. C'est facile! Je l'ai implémenté avec CGLib en tant que fournisseur de bytecode et cela fonctionne (assurez-vous de configurer lazy = "no-proxy" et fetch = "select", pas "join", dans votre HBM).

Je pense que c'est une bonne alternative à la véritable instrumentation (je veux dire automatique) lorsque vous avez une seule relation annulable un à un que vous voulez rendre paresseuse. Le principal inconvénient est que la solution dépend du fournisseur de bytecode que vous utilisez, alors commentez votre classe avec précision car vous pourriez avoir à changer le fournisseur de bytecode à l'avenir; bien sûr, vous modifiez également votre bean modèle pour une raison technique et ce n'est pas bien.

Pino
la source
1

Cette question est assez ancienne, mais avec Hibernate 5.1.10, il existe de nouvelles solutions plus confortables.

Le chargement différé fonctionne sauf pour le côté parent d'une association @OneToOne. En effet, Hibernate n'a aucun autre moyen de savoir s'il faut affecter un null ou un proxy à cette variable. Plus de détails que vous pouvez trouver dans cet article

  • Vous pouvez activer l'amélioration du bytecode de chargement paresseux
  • Ou, vous pouvez simplement supprimer le côté parent et utiliser le côté client avec @MapsId comme expliqué dans l'article ci-dessus. De cette façon, vous constaterez que vous n'avez pas vraiment besoin du côté parent puisque l'enfant partage le même identifiant avec le parent afin que vous puissiez facilement récupérer l'enfant en connaissant l'ID parent.
Toumi
la source
0

Si la relation ne doit pas être bidirectionnelle, une @ElementCollection peut être plus simple que d'utiliser une collection One2Many paresseuse.

Stefan
la source