Come posso rendere pigra una relazione JPA OneToOne


212

In questa applicazione che stiamo sviluppando, abbiamo notato che una vista era particolarmente lenta. Ho profilato la vista e ho notato che c'era una query eseguita da ibernazione che impiegava 10 secondi anche se c'erano solo due oggetti nel database da recuperare. Tutti OneToManye le ManyToManyrelazioni erano pigri, quindi non era questo il problema. Durante l'ispezione dell'effettivo SQL in esecuzione, ho notato che c'erano oltre 80 join nella query.

Ispezionando ulteriormente il problema, ho notato che il problema era causato dalla profonda gerarchia OneToOnee dalle ManyToOnerelazioni tra le classi di entità. Quindi, ho pensato, li renderò semplicemente pigri, questo dovrebbe risolvere il problema. Ma annotando uno @OneToOne(fetch=FetchType.LAZY)o@ManyToOne(fetch=FetchType.LAZY) sembra non funzionare. O ottengo un'eccezione o quindi non vengono effettivamente sostituiti con un oggetto proxy e quindi sono pigri.

Qualche idea su come farò funzionare? Nota che non uso il persistence.xmlper definire relazioni o dettagli di configurazione, tutto è fatto nel codice Java.

Risposte:


218

Prima di tutto, alcuni chiarimenti alla risposta di KLE :

  1. L'associazione uno-a-uno non vincolata (nullable) è l'unica che non può essere proxy senza la strumentazione bytecode. Il motivo di ciò è che l'entità proprietario DEVE sapere se la proprietà dell'associazione deve contenere un oggetto proxy o NULL e non può determinarlo osservando le colonne della sua tabella di base a causa del fatto che uno a uno è normalmente mappato tramite PK condiviso, quindi deve essere recuperato con impazienza in ogni caso rendendo inutile il proxy. Ecco una spiegazione più dettagliata .

  2. molte associazioni (e una a molte, ovviamente) non soffrono di questo problema. L'entità proprietario può facilmente controllare il proprio FK (e nel caso di uno-a-molti, il proxy di raccolta vuoto viene creato inizialmente e popolato su richiesta), quindi l'associazione può essere pigra.

  3. Sostituire uno a uno con uno a molti non è quasi mai una buona idea. Puoi sostituirlo con un unico molti-a-uno ma ci sono altre (forse migliori) opzioni.

Rob H. ha un punto valido, tuttavia potresti non essere in grado di implementarlo a seconda del tuo modello (ad es. Se la tua associazione one-to-one è nullable).

Ora, per quanto riguarda la domanda originale:

A) @ManyToOne(fetch=FetchType.LAZY)dovrebbe funzionare bene. Sei sicuro che non venga sovrascritto nella query stessa? È possibile specificare join fetchin HQL e / o impostare esplicitamente la modalità di recupero tramite l'API Criteria che avrebbe la precedenza sull'annotazione di classe. Se non è così e continui ad avere problemi, pubblica le tue classi, le tue query e il risultante SQL per conversazioni più mirate.

B) @OneToOneè più complicato. Se non è assolutamente nullable, segui il suggerimento di Rob H. e specificalo come tale:

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

Altrimenti, se è possibile modificare il database (aggiungere una colonna di chiave esterna alla tabella del proprietario), farlo e mapparlo come "unito":

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

e in OtherEntity:

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

Se non riesci a farlo (e non riesci a convivere con un impaziente recupero) la strumentazione bytecode è la tua unica opzione. Devo essere d'accordo con CPerkins , comunque - se ne hai 80 !!! si unisce a causa delle entusiaste associazioni OneToOne, hai problemi più grandi di questo :-)


Forse c'è un'altra opzione, ma non l'ho testato personalmente: da un lato non vincolato, usa a one-to-onecon una formula simileselect other_entity.id from other_entity where id = other_entity.id . Naturalmente, questo non è l'ideale per le prestazioni delle query.
Frédéric,

1
opzionale = falso, non funziona per me. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", opzionale = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts,

21

Per far funzionare il caricamento lento su mappature one-to-one nullable è necessario lasciare che l'ibernazione esegua la strumentazione del tempo di compilazione e aggiunga un@LazyToOne(value = LazyToOneOption.NO_PROXY) one-to-one.

Mappatura di esempio:

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

Esempio di estensione del file Ant Build (per eseguire la strumentazione del tempo di compilazione 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>

3
Perché LazyToOneOption.NO_PROXYe no LazyToOneOption.PROXY?
Telmo Marques,

Questo non risponde al "perché", ma questo fatto è affermato anche qui (verso la fine della sezione "Mappatura tipica"): vladmihalcea.com/…
DanielM

12

L'idea alla base di XToOnes a Hibernate è che nella maggior parte dei casi non sono pigri.

Una ragione è che, quando Hibernate deve decidere di mettere un proxy (con l'id) o un null,
deve comunque guardare nell'altra tabella per unirsi. Il costo di accesso all'altra tabella nel database è significativo, quindi potrebbe anche recuperare i dati per quella tabella in quel momento (comportamento non pigro), invece di recuperarli in una richiesta successiva che richiederebbe un secondo accesso al stesso tavolo.

Modificato: per dettagli, fare riferimento alla risposta di ChssPly76 . Questo è meno preciso e dettagliato, non ha nulla da offrire. Grazie ChssPly76.


Ci sono diverse cose che non vanno qui - ho fornito un'altra risposta di seguito con una spiegazione (troppe cose, non rientrano in un commento)
ChssPly76

8

Ecco qualcosa che ha funzionato per me (senza strumentazione):

Invece di usare @OneToOnesu entrambi i lati, uso @OneToManynella parte inversa della relazione (quella con mappedBy). Ciò rende la proprietà una raccolta (List nell'esempio seguente), ma la traduco in un elemento nel getter, rendendolo trasparente per i clienti.

Questa configurazione funziona pigramente, ovvero le selezioni vengono effettuate solo quando getPrevious()o getNext()vengono chiamate - e solo una selezione per ogni chiamata.

La struttura del tavolo:

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); }

}

7

Come ho spiegato in questo articolo , a meno che non si stia utilizzando Bytecode Enhancement , non è possibile recuperare pigramente il lato genitore@OneToOne associazione .

Tuttavia, molto spesso, non è nemmeno necessaria l'associazione lato genitore se si utilizza @MapsIdsul lato 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
}

Con @MapsId, ilid proprietà nella tabella figlio funge da chiave primaria e chiave esterna per la chiave primaria della tabella padre.

Pertanto, se si dispone di un riferimento Postall'entità padre , è possibile recuperare facilmente l'entità figlio utilizzando l'identificatore entità padre:

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

In questo modo, non si avranno problemi di query N + 1 che potrebbero essere causati mappedBy @OneToOnedall'associazione sul lato genitore.


in questo modo non possiamo più eseguire operazioni a cascata da genitore a figlio: /
Hamdi

Per persistere, è solo una chiamata persistente aggiuntiva, per l'eliminazione, è possibile utilizzare DDL in cascata.
Vlad Mihalcea,

6

Nei mapping XML Hibernate nativi, è possibile ottenere ciò dichiarando un mapping uno a uno con l' attributo vincolato impostato su true. Non sono sicuro di quale sia l'equivalente dell'annotazione Hibernate / JPA di ciò, e una rapida ricerca del documento non ha fornito alcuna risposta, ma spero che ciò ti dia un vantaggio.


5
+1 per un buon suggerimento; sfortunatamente non è sempre applicabile in quanto il modello di dominio potrebbe effettivamente richiedere nullabilità. Il modo corretto di mappare questo tramite annotazioni è@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76,

Ho provato questo e non ho visto alcun miglioramento delle prestazioni. Ho ancora visto molte query nell'output di ibernazione tramite il debugger.
P.Brian.Mackey,

3

Come già perfettamente spiegato da ChssPly76, i proxy di Hibernate non aiutano con associazioni one-to-one non vincolate (nullable), MA c'è un trucco spiegato qui per evitare di impostare la strumentazione. L'idea è di ingannare Hibernate che la classe di entità che vogliamo usare sia già stata strumentata: la strumentate manualmente nel codice sorgente. È facile! L'ho implementato con CGLib come provider bytecode e funziona (assicurati di configurare lazy = "no-proxy" e fetch = "select", non "join", nel tuo HBM).

Penso che questa sia una buona alternativa alla strumentazione reale (intendo automatica) quando hai solo una relazione nulla-a-una che vuoi rendere pigra. Lo svantaggio principale è che la soluzione dipende dal provider di bytecode che stai utilizzando, quindi commenta accuratamente la tua classe perché potresti dover cambiare il provider di bytecode in futuro; ovviamente, stai anche modificando il tuo bean di modello per un motivo tecnico e questo non va bene.


1

Questa domanda è piuttosto vecchia, ma con Hibernate 5.1.10, ci sono alcune nuove soluzioni più comode.

Il caricamento lento funziona ad eccezione del lato genitore di un'associazione @OneToOne. Questo perché Hibernate non ha altro modo di sapere se assegnare una null o un proxy a questa variabile. Maggiori dettagli sono disponibili in questo articolo

  • È possibile attivare il caricamento lento tramite il miglioramento del codice
  • In alternativa, puoi semplicemente rimuovere il lato genitore e utilizzare il lato client con @MapsId come spiegato nell'articolo sopra. In questo modo, scoprirai che non hai davvero bisogno del lato genitore poiché il figlio condivide lo stesso ID con il genitore in modo da poter facilmente recuperare il figlio conoscendo l'id genitore.

0

Se la relazione non deve essere bidirezionale, un @ElementCollection potrebbe essere più semplice rispetto all'utilizzo di una raccolta One2Many pigra.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.