Warisan JPA @EntityGraph mencakup asosiasi opsional subkelas

12

Dengan model domain berikut, saya ingin memuat semua Answers termasuk Values dan sub-anak mereka masing-masing dan memasukkannya AnswerDTOke dalam kemudian dikonversi ke JSON. Saya memiliki solusi yang berfungsi tetapi mengalami masalah N +1 yang ingin saya singkirkan dengan menggunakan ad-hoc @EntityGraph. Semua asosiasi dikonfigurasikan LAZY.

masukkan deskripsi gambar di sini

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Menggunakan ad-hoc @EntityGraphpada Repositorymetode ini saya dapat memastikan bahwa nilai-nilai sudah diambil sebelumnya untuk mencegah N + 1 pada Answer->Valueasosiasi. Sementara hasil saya baik-baik saja ada masalah N +1, karena malas memuat selectedasosiasi dari MCValues.

Menggunakan ini

@EntityGraph(attributePaths = {"value.selected"})

gagal, karena selectedbidang ini tentu saja hanya sebagian dari beberapa Valueentitas:

Unable to locate Attribute  with the the given name [selected] on this ManagedType [x.model.Value];

Bagaimana saya bisa memberi tahu JPA hanya mencoba mengambil selectedasosiasi jika nilainya adalah a MCValue? Saya butuh sesuatu seperti optionalAttributePaths.

Terjebak
sumber

Jawaban:

8

Anda hanya dapat menggunakan EntityGraphjika atribut asosiasi adalah bagian dari superclass dan juga bagian dari semua subclass. Kalau tidak, EntityGraphakan selalu gagal dengan Exceptionyang Anda dapatkan saat ini.

Cara terbaik untuk menghindari masalah pilih N + 1 Anda adalah dengan membagi kueri Anda menjadi 2 pertanyaan:

Kueri pertama mengambil MCValueentitas menggunakan EntityGraphuntuk mengambil asosiasi yang dipetakan oleh selectedatribut. Setelah permintaan itu, entitas-entitas ini kemudian disimpan dalam cache level 1 Hibernate / konteks persistensi. Hibernate akan menggunakannya ketika memproses hasil kueri ke-2.

@Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
@EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();

Kueri ke-2 kemudian mengambil Answerentitas dan menggunakan a EntityGraphuntuk juga mengambil Valueentitas terkait . Untuk setiap Valueentitas, Hibernate akan membuat instance subclass spesifik dan memeriksa apakah cache level 1 sudah berisi objek untuk kombinasi kelas dan kunci utama itu. Jika itu masalahnya, Hibernate menggunakan objek dari cache level 1 alih-alih data yang dikembalikan oleh kueri.

@Query("SELECT a FROM Answer a")
@EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();

Karena kami sudah mengambil semua MCValueentitas dengan selectedentitas terkait , kami sekarang mendapatkan Answerentitas dengan valueasosiasi yang diinisialisasi . Dan jika asosiasi tersebut mengandung MCValueentitas, selectedasosiasi tersebut juga akan diinisialisasi.

Thorben Janssen
sumber
Saya berpikir tentang memiliki dua pertanyaan, yang pertama untuk mengambil jawaban + nilai dan yang kedua untuk mengambil selectedjawaban yang memiliki a MCValue. Saya tidak suka bahwa ini akan memerlukan loop tambahan dan saya perlu mengelola pemetaan antara set data. Saya suka ide Anda untuk mengeksploitasi cache Hibernate untuk ini. Bisakah Anda menguraikan seberapa aman (dalam hal konsistensi) itu bergantung pada cache untuk mengandung hasil? Apakah ini berfungsi ketika kueri dibuat dalam transaksi? Saya takut sulit dikenali dan kesalahan inisialisasi malas sporadis.
Terjebak
1
Anda perlu melakukan kedua kueri dalam transaksi yang sama. Selama Anda melakukan itu, dan tidak menghapus konteks kegigihan Anda, itu benar-benar aman. Tembolok level 1 Anda akan selalu berisi MCValueentitas. Dan Anda tidak perlu loop tambahan. Anda harus mengambil semua MCValueentitas dengan 1 kueri yang bergabung ke Answerdan menggunakan klausa WHERE yang sama dengan kueri Anda saat ini. Saya juga membicarakan hal ini dalam siaran langsung hari ini: youtu.be/70B9znTmi00?t=238 Ini dimulai pada 3:58 tetapi saya mengambil beberapa pertanyaan lain di antaranya ...
Thorben Janssen
Bagus, terima kasih atas tindak lanjutnya! Saya juga ingin menambahkan, bahwa solusi ini membutuhkan 1 permintaan per Subkelas. Jadi rawatan tidak masalah bagi kami, tetapi solusi ini mungkin tidak cocok untuk semua kasus.
Terjebak
Saya perlu sedikit mengoreksi komentar terakhir saya: Tentu saja Anda hanya perlu kueri per subkelas yang mengalami masalah. Juga perlu dicatat, bahwa untuk atribut subclass ini sepertinya tidak menjadi masalah, karena penggunaan SINGLE_TABLE_INHERITANCE.
Terjebak
7

Saya tidak tahu apa yang dilakukan Spring-Data di sana, tetapi untuk melakukan itu, Anda biasanya harus menggunakan TREAToperator untuk dapat mengakses sub-asosiasi tetapi implementasi untuk Operator itu cukup bermasalah. Hibernate mendukung akses properti subtipe implisit yang Anda perlukan di sini, tetapi tampaknya Spring-Data tidak dapat menangani ini dengan benar. Saya dapat merekomendasikan agar Anda melihat Blaze-Persistence Entity-Views , perpustakaan yang berfungsi di atas JPA yang memungkinkan Anda memetakan struktur yang sewenang-wenang terhadap model entitas Anda. Anda dapat memetakan model DTO Anda dengan cara yang aman, juga struktur warisan. Tampilan entitas untuk use case Anda bisa terlihat seperti ini

@EntityView(Answer.class)
interface AnswerDTO {
  @IdMapping
  Long getId();
  ValueDTO getValue();
}
@EntityView(Value.class)
@EntityViewInheritance
interface ValueDTO {
  @IdMapping
  Long getId();
}
@EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
  String getText();
}
@EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
  int getRating();
}
@EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
  @Mapping("selected.id")
  Set<Long> getOption();
}

Dengan integrasi data pegas yang disediakan oleh Blaze-Persistence Anda dapat menentukan repositori seperti ini dan langsung menggunakan hasilnya

@Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
  List<AnswerDTO> findAll();
}

Ini akan menghasilkan kueri HQL yang memilih apa yang Anda petakan yang AnswerDTOmana adalah sesuatu seperti berikut ini.

SELECT
  a.id, 
  v.id,
  TYPE(v), 
  CASE WHEN TYPE(v) = TextValue THEN v.text END,
  CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
  CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
Christian Beikov
sumber
Hmm terima kasih atas petunjuk ke perpustakaan Anda yang telah saya temukan, tetapi kami tidak akan menggunakannya untuk 2 alasan utama: 1) kami tidak dapat mengandalkan lib yang akan didukung selama masa proyek kami (blazebit perusahaan Anda agak kecil dan di permulaannya). 2) Kami tidak akan berkomitmen untuk tumpukan teknologi yang lebih kompleks untuk mengoptimalkan satu permintaan. (Saya tahu bahwa lib Anda dapat melakukan lebih banyak, tetapi kami lebih suka tumpukan teknologi umum dan lebih baik hanya mengimplementasikan permintaan kustom / transformasi jika tidak ada solusi JPA).
Terjebak
1
Blaze-Persistence adalah open source dan Entity-Views lebih atau kurang diimplementasikan di atas JPQL / HQL yang standar. Fitur yang diterapkannya stabil dan masih akan berfungsi dengan versi Hibernate masa depan, karena berfungsi di atas standar. Saya memahami bahwa Anda tidak ingin memperkenalkan sesuatu karena satu kasus penggunaan, tapi saya ragu itu satu-satunya kasus penggunaan yang Anda bisa gunakan Tampilan Entitas. Memperkenalkan Entity Views biasanya mengarah pada pengurangan jumlah kode boilerplate secara signifikan dan juga meningkatkan kinerja kueri. Jika Anda tidak ingin menggunakan alat yang membantu Anda, biarlah.
Christian Beikov
Setidaknya Anda meremehkan masalah dan Anda memberikan solusi. Jadi Anda mendapatkan hadiah meskipun jawaban tidak menjelaskan apa yang terjadi di masalah awal dan bagaimana JPA bisa menyelesaikannya. Dari persepsi saya itu tidak didukung oleh JPA dan itu harus menjadi permintaan fitur. Saya akan menawarkan hadiah lain untuk jawaban yang lebih rumit yang menargetkan JPA saja.
Terjebak
Sama sekali tidak mungkin dengan JPA. Anda memerlukan operator TREAT yang tidak sepenuhnya didukung di penyedia JPA mana pun, juga tidak didukung dalam anotasi EntityGraph. Jadi satu-satunya cara Anda dapat memodelkan ini adalah melalui fitur penyelesaian properti subtipe Hibernate implisit, yang mengharuskan Anda untuk menggunakan gabungan eksplisit.
Christian Beikov
1
Dalam jawaban Anda definisi tampilan harusinterface MCValueDTO extends ValueDTO { @Mapping("selected.id") Set<Long> getOption(); }
Terjebak
0

Proyek terbaru saya menggunakan GraphQL (yang pertama untuk saya) dan kami memiliki masalah besar dengan N + 1 kueri dan mencoba untuk mengoptimalkan kueri untuk hanya bergabung dengan tabel ketika mereka diperlukan. Saya telah menemukan Cosium / spring-data-jpa-entitas-graph tak tergantikan. Itu memperluas JpaRepositorydan menambahkan metode untuk meneruskan dalam grafik entitas ke kueri. Anda kemudian dapat membuat grafik entitas dinamis saat runtime untuk menambahkan gabungan kiri hanya untuk data yang Anda butuhkan.

Alur data kami terlihat seperti ini:

  1. Terima permintaan GraphQL
  2. Parse GraphQL meminta dan mengonversi ke daftar node grafik entitas dalam kueri
  3. Buat grafik entitas dari node yang ditemukan dan meneruskan ke repositori untuk dieksekusi

Untuk mengatasi masalah tidak termasuk node yang tidak valid ke dalam grafik entitas (misalnya __typenamedari graphql), saya membuat kelas utilitas yang menangani pembuatan grafik entitas. Kelas panggilan lewat dalam nama kelas yang menghasilkan grafik, yang kemudian memvalidasi setiap node dalam grafik terhadap metamodel yang dikelola oleh ORM. Jika simpul tidak ada dalam model, simpul itu akan dihapus dari daftar simpul grafik. (Pemeriksaan ini perlu rekursif dan periksa setiap anak juga)

Sebelum menemukan ini, saya telah mencoba proyeksi dan setiap alternatif lain yang direkomendasikan dalam dokumen Spring JPA / Hibernate, tetapi sepertinya tidak ada yang menyelesaikan masalah dengan elegan atau setidaknya dengan satu ton kode tambahan

aarbor
sumber
bagaimana cara mengatasi masalah memuat asosiasi yang tidak diketahui dari tipe super? Juga, seperti yang dikatakan pada jawaban lain, kami ingin tahu apakah ada solusi JPA murni, tapi saya juga berpikir bahwa lib menderita masalah yang sama bahwa selectedasosiasi tidak tersedia untuk semua jenis sub value.
Terjebak
Jika Anda tertarik dengan GraphQL, kami juga memiliki integrasi Blaze-Persistence Entity Views dengan graphql-java: persistence.blazebit.com/documentation/1.5/entity-view/manual/…
Christian Beikov
@ChristianBeikov terima kasih tetapi kami menggunakan SQPR untuk menghasilkan skema kami secara terprogram dari model / metode kami
aarbor
Jika Anda menyukai pendekatan kode-pertama, Anda akan menyukai integrasi GraphQL. Ini menangani mengambil hanya kolom / ekspresi yang benar-benar mengurangi joins dll secara otomatis.
Christian Beikov
0

Diedit setelah komentar Anda:

Saya minta maaf, saya belum memahami masalah Anda di babak pertama, masalah Anda muncul saat startup pegas-data, tidak hanya ketika Anda mencoba memanggil findAll ().

Jadi, Anda sekarang dapat menavigasi contoh lengkap dapat menarik dari github saya: https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/

Anda dapat dengan mudah mereproduksi dan memperbaiki masalah Anda di dalam proyek ini.

Secara efektif, data Pegas dan hibernasi tidak dapat menentukan grafik "terpilih" secara default dan Anda perlu menentukan cara untuk mengumpulkan opsi yang dipilih.

Jadi pertama-tama, Anda harus mendeklarasikan NamedEntityGraphs dari Jawaban kelas

Seperti yang Anda lihat, ada dua NamedEntityGraph untuk atribut nilai dari kelas Jawaban

  • Yang pertama untuk semua Nilai tanpa hubungan khusus untuk dimuat

  • Yang kedua untuk nilai Multichoice tertentu . Jika Anda menghapus yang ini, Anda mereproduksi pengecualian.

Kedua, Anda harus berada dalam konteks transaksional answerRepository.findAll () jika Anda ingin mengambil data dalam tipe LAZY

@Entity
@Table(name = "answer")
@NamedEntityGraphs({
    @NamedEntityGraph(
            name = "graph.Answer", 
            attributeNodes = @NamedAttributeNode(value = "value")
    ),
    @NamedEntityGraph(
            name = "graph.AnswerMultichoice",
            attributeNodes = @NamedAttributeNode(value = "value"),
            subgraphs = {
                    @NamedSubgraph(
                            name = "graph.AnswerMultichoice.selected",
                            attributeNodes = {
                                    @NamedAttributeNode("selected")
                            }
                    )
            }
    )
}
)
public class Answer
{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(updatable = false, nullable = false)
    private int id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "value_id", referencedColumnName = "id")
    private Value value;
// ..
}
bdzzaid
sumber
Masalahnya tidak mengambil yang value-association dari Answertapi mendapatkan selectedasosiasi dalam kasus ini valueadalah MCValue. Jawaban Anda tidak termasuk informasi mengenai itu.
Terjebak
@ Terjebak Terima kasih atas jawaban Anda, bisakah Anda membagikan kepada saya kelas MCValue, saya akan mencoba mereproduksi masalah Anda secara lokal.
bdzzaid
Contoh Anda hanya berfungsi karena Anda mendefinisikan asosiasi OneToManysebagai FetchType.EAGERtetapi sebagaimana dinyatakan dalam pertanyaan: semua asosiasi adalah LAZY.
Terjebak
@Stuck Saya memperbarui jawaban saya sejak pembaruan terakhir Anda, harap tahu jawaban saya akan membantu Anda menyelesaikan masalah Anda, dan membantu Anda memahami cara memuat grafik en entitas termasuk hubungan opsional.
bdzzaid
"Solusi" Anda masih mengalami masalah N + 1 asli yang menjadi pertanyaan pertanyaan ini: masukkan metode insert dan temukan dalam berbagai transaksi pengujian Anda dan Anda melihat bahwa jpa akan mengeluarkan permintaan DB selecteduntuk setiap jawaban alih-alih memuatnya di muka.
Terjebak