Bagaimana saya bisa membuat hubungan JPA OneToOne malas


212

Dalam aplikasi yang kami kembangkan ini, kami memperhatikan bahwa suatu tampilan sangat lambat. Saya membuat profil tampilan dan memperhatikan bahwa ada satu query yang dieksekusi oleh hibernate yang membutuhkan waktu 10 detik bahkan jika hanya ada dua objek dalam database untuk diambil. Semua OneToManydan ManyToManyhubungan itu malas jadi itu bukan masalah. Ketika memeriksa SQL yang sebenarnya dieksekusi, saya perhatikan bahwa ada lebih dari 80 bergabung dalam permintaan.

Lebih lanjut memeriksa masalah ini, saya perhatikan bahwa masalah itu disebabkan oleh hierarki yang mendalam OneToOnedan ManyToOnehubungan antara kelas entitas. Jadi, saya pikir, saya hanya akan membuat mereka menjadi malas, yang seharusnya bisa menyelesaikan masalah. Tetapi memberi anotasi baik @OneToOne(fetch=FetchType.LAZY)atau @ManyToOne(fetch=FetchType.LAZY)tidak berhasil. Entah saya mendapatkan pengecualian atau kemudian mereka tidak benar-benar diganti dengan objek proxy dan karenanya menjadi malas.

Ada ide bagaimana saya akan membuatnya bekerja? Perhatikan bahwa saya tidak menggunakan persistence.xmluntuk mendefinisikan relasi atau detail konfigurasi, semuanya dilakukan dalam kode java.

Jawaban:


218

Pertama, beberapa klarifikasi untuk jawaban KLE :

  1. Asosiasi tak terbatas (nullable) satu-ke-satu adalah satu-satunya yang tidak dapat diproksi tanpa instrumentasi bytecode. Alasan untuk ini adalah bahwa entitas pemilik HARUS tahu apakah properti asosiasi harus berisi objek proxy atau NULL dan tidak dapat menentukan bahwa dengan melihat kolom tabel dasarnya karena satu-ke-satu yang biasanya dipetakan melalui PK bersama, sehingga harus bersemangat diambil pula membuat proxy tidak berguna. Berikut penjelasan yang lebih detail .

  2. banyak-ke-satu asosiasi (dan satu-ke-banyak, jelas) tidak menderita masalah ini. Entitas pemilik dapat dengan mudah memeriksa FK-nya sendiri (dan dalam kasus satu-ke-banyak, proksi pengumpulan kosong dibuat pada awalnya dan diisi sesuai permintaan), sehingga asosiasi dapat menjadi malas.

  3. Mengganti satu-ke-satu dengan satu-ke-banyak hampir tidak pernah merupakan ide yang baik. Anda dapat menggantinya dengan banyak-ke-satu yang unik tetapi ada opsi lain (mungkin lebih baik).

Rob H. memiliki poin yang valid, namun Anda mungkin tidak dapat mengimplementasikannya tergantung pada model Anda (misalnya jika asosiasi satu-ke-satu Anda dapat dibatalkan).

Sekarang, sejauh pertanyaan awal:

A) @ManyToOne(fetch=FetchType.LAZY)harus bekerja dengan baik. Apakah Anda yakin itu tidak ditimpa dalam kueri itu sendiri? Dimungkinkan untuk menentukan join fetchdalam HQL dan / atau secara eksplisit mengatur mode pengambilan melalui API Kriteria yang akan diutamakan daripada anotasi kelas. Jika bukan itu masalahnya dan Anda masih mengalami masalah, silakan kirim kelas, kueri, dan hasilkan SQL untuk percakapan lebih lanjut.

B) @OneToOnelebih rumit. Jika jelas tidak dapat dibatalkan, ikuti saran Rob H. dan tentukan sebagai berikut:

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

Jika tidak, jika Anda dapat mengubah basis data Anda (tambahkan kolom kunci asing ke tabel pemilik), lakukan dan petakan sebagai "bergabung":

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

dan di OtherEntity:

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

Jika Anda tidak bisa melakukan itu (dan tidak bisa hidup dengan bersemangat mengambil) instrumentasi bytec adalah satu-satunya pilihan Anda. Namun saya harus setuju dengan CPerkins - jika Anda punya 80 !!! bergabung karena asosiasi OneToOne yang bersemangat, Anda punya masalah yang lebih besar maka ini :-)


Mungkin ada pilihan lain, tapi saya belum mengujinya secara pribadi: di sisi yang tidak dibatasi, gunakan one-to-onedengan rumus seperti select other_entity.id from other_entity where id = other_entity.id. Tentu saja, ini tidak ideal untuk kinerja permintaan.
Frédéric

1
opsional = salah, tidak berfungsi untuk saya. @OneToOne (fetch = FetchType.LAZY, mappedBy = "fundSeries", opsional = false) private FundSeriesDetailEntity fundSeriesDetail;
Oleg Kuts

21

Agar pemuatan malas berfungsi pada pemetaan satu-ke-satu yang dapat dibatalkan, Anda harus membiarkan hibernate berfungsi kompilasi instrumentasi waktu dan menambahkan @LazyToOne(value = LazyToOneOption.NO_PROXY)relasi satu-ke-satu.

Contoh Pemetaan:

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

Contoh ekstensi file Ant Build (untuk melakukan instrumentasi waktu kompilasi 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
Kenapa LazyToOneOption.NO_PROXYdan tidak LazyToOneOption.PROXY?
Telmo Marques

Ini tidak menjawab "mengapa", tetapi fakta ini juga dinyatakan di sini (menjelang akhir bagian "Pemetaan Khas"): vladmihalcea.com/…
DanielM

12

Gagasan dasar mengalahkan XToOnes di Hibernate adalah bahwa mereka tidak malas dalam banyak kasus.

Salah satu alasannya adalah, ketika Hibernate harus memutuskan untuk meletakkan proxy (dengan id) atau null,
itu tetap harus melihat ke tabel lainnya untuk bergabung. Biaya untuk mengakses tabel lain dalam database adalah signifikan, sehingga mungkin juga mengambil data untuk tabel itu pada saat itu (perilaku tidak malas), daripada mengambilnya dalam permintaan nanti yang akan membutuhkan akses kedua ke meja yang sama.

Diedit: untuk detail, silakan merujuk ke jawaban ChssPly76 . Yang ini kurang akurat dan detail, tidak ada yang bisa ditawarkan. Terima kasih ChssPly76.


Ada beberapa hal yang salah di sini - saya telah memberikan jawaban lain di bawah ini dengan penjelasan (terlalu banyak hal, tidak akan cocok dengan komentar)
ChssPly76

8

Inilah sesuatu yang telah bekerja untuk saya (tanpa instrumentasi):

Alih-alih menggunakan @OneToOnedi kedua sisi, saya menggunakan @OneToManydi bagian terbalik dari hubungan (yang dengan mappedBy). Itu membuat properti koleksi ( Listdalam contoh di bawah), tapi saya menerjemahkannya menjadi item di pengambil, membuatnya transparan kepada klien.

Pengaturan ini bekerja dengan malas, yaitu, pemilihan hanya dilakukan ketika getPrevious()atau getNext()dipanggil - dan hanya satu pilih untuk setiap panggilan.

Struktur tabel:

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

Kelas:

@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

Seperti yang saya jelaskan di artikel ini , kecuali jika Anda menggunakan Bytecode Enhancement , Anda tidak dapat mengambil dengan malas @OneToOneasosiasi sisi orangtua .

Namun, paling sering, Anda bahkan tidak memerlukan asosiasi sisi orang tua jika Anda gunakan @MapsIddi sisi klien:

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

Dengan @MapsId, ituid properti di tabel anak berfungsi sebagai Primary Key dan Foreign Key ke tabel induk Primary Key.

Jadi, jika Anda memiliki referensi ke Postentitas induk , Anda dapat dengan mudah mengambil entitas anak menggunakan pengidentifikasi entitas induk:

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

Dengan cara ini, Anda tidak akan memiliki masalah kueri N +1 yang dapat disebabkan oleh mappedBy @OneToOneasosiasi di sisi induk.


dengan cara ini kita tidak dapat lagi menjalankan operasi dari orang tua ke anak: /
Hamdi

Untuk bertahan, itu hanya panggilan bertahan ekstra, untuk menghapus, Anda dapat menggunakan kaskade DDL.
Vlad Mihalcea

6

Di pemetaan XML Hibernate asli, Anda bisa melakukannya dengan mendeklarasikan pemetaan satu-ke-satu dengan atribut terbatas yang disetel ke true. Saya tidak yakin apa yang setara dengan penjelasan Hibernate / JPA itu, dan pencarian cepat dari dokumen tidak memberikan jawaban, tapi mudah-mudahan itu memberi Anda petunjuk untuk melanjutkan.


5
+1 untuk saran yang bagus; Sayangnya itu tidak selalu berlaku karena model domain sebenarnya mungkin memerlukan nullability. Cara yang tepat untuk memetakan ini melalui anotasi adalah@OneToOne(optional=false,fetch=FetchMode.LAZY)
ChssPly76

Saya mencoba ini dan tidak melihat peningkatan kinerja. Saya masih melihat banyak pertanyaan dalam output hibernate melalui debugger.
P.Brian.Mackey

3

Seperti yang sudah dijelaskan dengan sempurna oleh ChssPly76, proksi Hibernate tidak membantu dengan asosiasi satu-ke-satu yang tidak dibatasi (nullable), TETAPI ada trik yang dijelaskan di sini untuk menghindari mengatur instrumentasi. Idenya adalah untuk menipu Hibernate bahwa kelas entitas yang ingin kita gunakan telah diinstrumentasi: Anda instrumen secara manual dalam kode sumber. Mudah! Saya telah mengimplementasikannya dengan CGLib sebagai penyedia bytecode dan berfungsi (pastikan Anda mengkonfigurasi lazy = "no-proxy" dan fetch = "pilih", bukan "gabung", di HBM Anda).

Saya pikir ini adalah alternatif yang baik untuk instrumentasi nyata (maksud saya otomatis) ketika Anda hanya memiliki satu hubungan nullable satu yang Anda ingin membuat malas. Kelemahan utama adalah bahwa solusinya tergantung pada penyedia bytecode yang Anda gunakan, jadi komentar kelas Anda secara akurat karena Anda bisa mengubah penyedia bytecode di masa depan; tentu saja, Anda juga memodifikasi kacang model Anda karena alasan teknis dan ini tidak baik.


1

Pertanyaan ini cukup lama, tetapi dengan Hibernate 5.1.10, ada beberapa solusi baru yang lebih baik dan nyaman.

Pemuatan malas berfungsi kecuali untuk sisi induk dari asosiasi @OneToOne. Ini karena Hibernate tidak memiliki cara lain untuk mengetahui apakah akan menetapkan nol atau Proxy ke variabel ini. Rincian lebih lanjut dapat Anda temukan di artikel ini

  • Anda dapat mengaktifkan peningkatan bytecode pemuatan malas
  • Atau, Anda bisa menghapus sisi induk dan menggunakan sisi klien dengan @MapsId seperti yang dijelaskan dalam artikel di atas. Dengan cara ini, Anda akan menemukan bahwa Anda tidak benar-benar membutuhkan sisi induk karena anak tersebut berbagi id yang sama dengan orangtua sehingga Anda dapat dengan mudah menjemput anak itu dengan mengetahui id induknya.

0

Jika hubungannya tidak boleh dua arah, maka @ElementCollection mungkin lebih mudah daripada menggunakan koleksi One2Many yang malas.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.