Wie kann ich eine JPA OneToOne-Beziehung faul machen?


212

In dieser Anwendung, die wir entwickeln, haben wir festgestellt, dass eine Ansicht besonders langsam war. Ich habe die Ansicht profiliert und festgestellt, dass im Ruhezustand eine Abfrage ausgeführt wurde, die 10 Sekunden dauerte, selbst wenn nur zwei Objekte in der Datenbank abgerufen werden mussten. Alle OneToManyund ManyToManyBeziehungen waren faul, also war das nicht das Problem. Bei der Überprüfung der tatsächlich ausgeführten SQL habe ich festgestellt, dass die Abfrage über 80 Verknüpfungen enthält.

Bei einer weiteren Untersuchung des Problems stellte ich fest, dass das Problem durch die tiefe Hierarchie OneToOneund die ManyToOneBeziehungen zwischen Entitätsklassen verursacht wurde. Also, dachte ich, ich mache sie nur faul, das sollte das Problem lösen. Aber entweder zu kommentieren @OneToOne(fetch=FetchType.LAZY)oder @ManyToOne(fetch=FetchType.LAZY)nicht zu funktionieren scheint. Entweder bekomme ich eine Ausnahme oder sie werden nicht durch ein Proxy-Objekt ersetzt und sind daher faul.

Irgendwelche Ideen, wie ich das zum Laufen bringen kann? Beachten Sie, dass ich das nicht verwende persistence.xml, um Beziehungen oder Konfigurationsdetails zu definieren. Alles wird in Java-Code ausgeführt.

Antworten:


218

Zunächst einige Klarstellungen zu KLEs Antwort:

  1. Die uneingeschränkte (nullfähige) Eins-zu-Eins-Zuordnung ist die einzige, die ohne Bytecode-Instrumentierung nicht als Proxy verwendet werden kann. Der Grund dafür ist, dass die Eigentümerentität wissen MUSS, ob die Zuordnungseigenschaft ein Proxy-Objekt oder NULL enthalten soll, und dies nicht durch Betrachten der Spalten der Basistabelle feststellen kann, da die Eins-zu-Eins-Zuordnung normalerweise über eine gemeinsam genutzte PK erfolgt muss sowieso eifrig geholt werden, was Proxy sinnlos macht. Hier ist eine detailliertere Erklärung.

  2. Viele-zu-Eins-Assoziationen (und natürlich Eins-zu-Viele) leiden nicht unter diesem Problem. Die Eigentümerentität kann problemlos ihre eigene FK überprüfen (und im Fall von Eins-zu-Viele wird zunächst ein leerer Sammlungsproxy erstellt und bei Bedarf ausgefüllt), sodass die Zuordnung faul sein kann.

  3. Eins-zu-eins durch eins-zu-viele zu ersetzen, ist so gut wie nie eine gute Idee. Sie können es durch eindeutige Viele-zu-Eins-Optionen ersetzen, es gibt jedoch auch andere (möglicherweise bessere) Optionen.

Rob H. hat einen wichtigen Punkt, aber Sie können nicht in der Lage sein , es zu implementieren je nach Modell (zB wenn Sie eine Eins-zu-Eins - Zuordnung ist nullable).

Nun zur ursprünglichen Frage:

A) @ManyToOne(fetch=FetchType.LAZY)sollte gut funktionieren. Sind Sie sicher, dass es in der Abfrage selbst nicht überschrieben wird? Es ist möglich, join fetchin HQL anzugeben und / oder den Abrufmodus explizit über die Kriterien-API festzulegen, die Vorrang vor Klassenanmerkungen hat. Wenn dies nicht der Fall ist und Sie immer noch Probleme haben, veröffentlichen Sie bitte Ihre Klassen, Abfragen und das daraus resultierende SQL, um eine genauere Konversation zu erhalten.

B) @OneToOneist schwieriger. Wenn es definitiv nicht nullbar ist, folgen Sie dem Vorschlag von Rob H. und geben Sie ihn als solchen an:

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

Wenn Sie andernfalls Ihre Datenbank ändern können (fügen Sie der Besitzertabelle eine Fremdschlüsselspalte hinzu), tun Sie dies und ordnen Sie sie als "verbunden" zu:

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

und in OtherEntity:

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

Wenn Sie das nicht können (und nicht mit eifrigem Abrufen leben können), ist die Bytecode-Instrumentierung Ihre einzige Option. Ich muss CPerkins jedoch zustimmen - wenn Sie 80 haben !!! tritt aufgrund eifriger OneToOne-Assoziationen bei, Sie haben größere Probleme als diese :-)


Vielleicht gibt es eine andere Option, aber ich habe sie nicht persönlich getestet: Verwenden Sie auf einer nicht eingeschränkten Seite eine one-to-onemit einer Formel wie select other_entity.id from other_entity where id = other_entity.id. Dies ist natürlich nicht ideal für Abfrageleistungen.
Frédéric

1
optional = false, funktioniert bei mir nicht. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", optional = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts

21

Damit das verzögerte Laden mit nullbaren Eins-zu-Eins-Zuordnungen funktioniert , müssen Sie den Ruhezustand die Instrumentierung zur Kompilierungszeit durchführen lassen und @LazyToOne(value = LazyToOneOption.NO_PROXY)der Eins-zu-Eins-Beziehung ein hinzufügen .

Beispielzuordnung:

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

Beispiel für eine Ant Build-Dateierweiterung (für die Instrumentierung der Kompilierungszeit im Ruhezustand):

<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
Warum LazyToOneOption.NO_PROXYund nicht LazyToOneOption.PROXY?
Telmo Marques

Dies beantwortet nicht das "Warum", aber diese Tatsache wird auch hier behauptet (gegen Ende des Abschnitts "Typisches Mapping"): vladmihalcea.com/…
DanielM

12

Die Grundidee der XToOnes im Ruhezustand ist, dass sie in den meisten Fällen nicht faul sind.

Ein Grund dafür ist, dass Hibernate, wenn es sich entscheiden muss, einen Proxy (mit der ID) oder eine Null zu setzen,
ohnehin in die andere Tabelle schauen muss, um Mitglied zu werden. Die Kosten für den Zugriff auf die andere Tabelle in der Datenbank sind erheblich. Daher können die Daten für diese Tabelle zu diesem Zeitpunkt abgerufen werden (nicht verzögertes Verhalten), anstatt sie in einer späteren Anforderung abzurufen, für die ein zweiter Zugriff auf die erforderlich wäre gleiche Tabelle.

Bearbeitet: Einzelheiten finden Sie in der Antwort von ChssPly76 . Dieser ist weniger genau und detailliert, er hat nichts zu bieten. Danke ChssPly76.


Hier sind einige Dinge falsch - ich habe unten eine andere Antwort mit einer Erklärung gegeben (zu viel Zeug, passt nicht in einen Kommentar)
ChssPly76

8

Hier ist etwas, das für mich funktioniert hat (ohne Instrumentierung):

Anstatt @OneToOneauf beiden Seiten zu verwenden, verwende ich @OneToManyim umgekehrten Teil der Beziehung (der mit mappedBy). Das macht die Eigenschaft zu einer Sammlung ( Listim folgenden Beispiel), aber ich übersetze sie in ein Element im Getter, um sie für die Clients transparent zu machen.

Dieses Setup funktioniert träge, dh die Auswahl erfolgt nur, wenn getPrevious()oder getNext()aufgerufen wird - und nur eine Auswahl für jeden Anruf.

Die Tabellenstruktur:

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

Die Klasse:

@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

Wie in diesem Artikel erläutert , können Sie die übergeordnete Zuordnung nur dann träge abrufen , wenn Sie die Bytecode-Verbesserung verwenden@OneToOne .

In den meisten Fällen benötigen Sie jedoch nicht einmal die übergeordnete Zuordnung, wenn Sie sie @MapsIdauf der Clientseite verwenden:

@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
}

Mit dient @MapsIddie idEigenschaft in der untergeordneten Tabelle sowohl als Primärschlüssel als auch als Fremdschlüssel für den Primärschlüssel der übergeordneten Tabelle.

Wenn Sie also einen Verweis auf die übergeordnete PostEntität haben, können Sie die untergeordnete Entität mithilfe der übergeordneten Entitätskennung einfach abrufen:

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

Auf diese Weise treten keine N + 1-Abfrageprobleme auf , die durch die mappedBy @OneToOneZuordnung auf der übergeordneten Seite verursacht werden könnten .


Auf diese Weise können wir Operationen nicht mehr von Eltern zu Kind kaskadieren: /
Hamdi

Für persist ist es nur ein zusätzlicher persist-Aufruf, für delete können Sie eine DDL-Kaskade verwenden.
Vlad Mihalcea

6

In nativen XML-Zuordnungen im Ruhezustand können Sie dies erreichen, indem Sie eine Eins-zu-Eins- Zuordnung mit dem eingeschränkten Attribut true deklarieren . Ich bin mir nicht sicher, was das Äquivalent der Hibernate / JPA-Annotation dazu ist, und eine schnelle Suche im Dokument lieferte keine Antwort, aber hoffentlich gibt Ihnen dies einen Anhaltspunkt, um fortzufahren.


5
+1 für einen guten Vorschlag; Leider ist dies nicht immer anwendbar, da das Domain-Modell möglicherweise eine Nullfähigkeit erfordert. Der richtige Weg, dies über Anmerkungen @OneToOne(optional=false,fetch=FetchMode.LAZY)
abzubilden,

Ich habe es versucht und keine Leistungsverbesserung festgestellt. Ich habe immer noch viele Abfragen in der Ruhezustandsausgabe über den Debugger gesehen.
P.Brian.Mackey

3

Wie bereits perfekt durch ChssPly76, Hibernate erklärt Proxies nicht helfen , mit unbeschränkten (nullable) eine Eins-zu-Eins - Verbände, aber es gibt einen Trick erklärt hier zu vermeiden Instrumentierung einzurichten. Die Idee ist, Hibernate zu täuschen, dass die Entitätsklasse, die wir verwenden möchten, bereits instrumentiert wurde: Sie instrumentieren sie manuell im Quellcode. Es ist einfach! Ich habe es mit CGLib als Bytecode-Anbieter implementiert und es funktioniert (stellen Sie sicher, dass Sie in Ihrem HBM lazy = "no-proxy" und fetch = "select" konfigurieren, nicht "join").

Ich denke, dies ist eine gute Alternative zur echten (ich meine automatischen) Instrumentierung, wenn Sie nur eine Eins-zu-Eins-Null-Beziehung haben, die Sie faul machen möchten. Der Hauptnachteil besteht darin, dass die Lösung von dem von Ihnen verwendeten Bytecode-Anbieter abhängt. Kommentieren Sie Ihre Klasse daher genau, da Sie den Bytecode-Anbieter möglicherweise in Zukunft ändern müssen. Natürlich modifizieren Sie Ihre Modellbohne auch aus technischen Gründen, und das ist nicht in Ordnung.


1

Diese Frage ist ziemlich alt, aber mit Hibernate 5.1.10 gibt es einige neue, komfortablere Lösungen.

Das verzögerte Laden funktioniert mit Ausnahme der übergeordneten Seite einer @ OneToOne-Zuordnung. Dies liegt daran, dass der Ruhezustand keine andere Möglichkeit hat, zu wissen, ob dieser Variablen eine Null oder ein Proxy zugewiesen werden soll. Weitere Details finden Sie in diesem Artikel

  • Sie können die Verbesserung des Bytecodes für das verzögerte Laden aktivieren
  • Sie können auch einfach die übergeordnete Seite entfernen und die Clientseite mit @MapsId verwenden, wie im obigen Artikel erläutert. Auf diese Weise werden Sie feststellen, dass Sie die übergeordnete Seite nicht wirklich benötigen, da das Kind dieselbe ID mit dem Elternteil teilt, sodass Sie das Kind leicht abrufen können, indem Sie die übergeordnete ID kennen.

0

Wenn die Beziehung nicht bidirektional sein darf, ist eine @ ElementCollection möglicherweise einfacher als die Verwendung einer faulen One2Many-Sammlung.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.