Hibernate melempar MultipleBagFetchException - tidak dapat secara bersamaan mengambil beberapa tas

471

Hibernate melempar pengecualian ini selama pembuatan SessionFactory:

org.hibernate.loader.MultipleBagFetchException: tidak dapat secara bersamaan mengambil beberapa tas

Ini adalah ujian saya:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 // @IndexColumn(name="INDEX_COL") if I had this the problem solve but I retrieve more children than I have, one child is null.
 private List<Child> children;

}

Child.java

@Entity
public Child {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private Parent parent;

}

Bagaimana dengan masalah ini? Apa yang dapat saya?


EDIT

OK, masalah yang saya miliki adalah entitas "induk" lain ada di dalam orangtua saya, perilaku saya yang sebenarnya adalah ini:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private AnotherParent anotherParent;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<Child> children;

}

AnotherParent.java

@Entity
public AnotherParent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<AnotherChild> anotherChildren;

}

Hibernate tidak suka dengan dua koleksi FetchType.EAGER, tapi ini sepertinya bug, saya tidak melakukan hal-hal yang tidak biasa ...

Menghapus FetchType.EAGERdari Parentatau AnotherParentmemecahkan masalah, tapi saya membutuhkannya, jadi solusi sebenarnya adalah dengan menggunakan @LazyCollection(LazyCollectionOption.FALSE)alih-alih FetchType(terima kasih kepada Bozho untuk solusinya).

pukulan
sumber
Saya akan bertanya, apa query SQL yang Anda harapkan untuk menghasilkan yang akan mengambil dua koleksi terpisah secara bersamaan? Jenis-jenis SQL yang akan dapat mencapai ini akan memerlukan gabungan cartesian (berpotensi sangat tidak efisien) atau UNION kolom terpisah (juga jelek). Agaknya ketidakmampuan untuk mencapai hal ini dalam SQL secara bersih & efisien memengaruhi desain API.
Thomas W
@ThomasW Ini adalah kueri sql yang harus dihasilkan:select * from master; select * from child1 where master_id = :master_id; select * from child2 where master_id = :master_id
nurettin
1
Anda bisa mendapatkan kesalahan simillar jika Anda memiliki lebih dari satu List<child>dengan fetchTypedidefinisikan lebih dari satu List<clield>
Big Zed

Jawaban:

555

Saya pikir versi hibernate yang lebih baru (mendukung JPA 2.0) harus menangani ini. Tetapi jika tidak, Anda dapat mengatasinya dengan menganotasi bidang koleksi dengan:

@LazyCollection(LazyCollectionOption.FALSE)

Ingatlah untuk menghapus fetchTypeatribut dari @*ToManyanotasi.

Tetapi perhatikan bahwa dalam kebanyakan kasus a Set<Child>lebih tepat daripada List<Child>, jadi kecuali Anda benar List- benar membutuhkanSet

Tetapi ingatkan bahwa dengan menggunakan set Anda tidak akan menghilangkan Produk Cartesian yang mendasari seperti yang dijelaskan oleh Vlad Mihalcea dalam jawabannya !

Bozho
sumber
4
aneh, itu berhasil bagi saya. Apakah Anda menghapus fetchTypedari @*ToMany?
Bozho
101
masalahnya adalah bahwa penjelasan JPA diuraikan untuk tidak memungkinkan lebih dari 2 koleksi bersemangat dimuat. Tetapi anotasi khusus hibernasi memungkinkannya.
Bozho
14
Kebutuhan akan lebih dari 1 EAGER tampaknya benar-benar realistis. Apakah batasan ini hanya pengawasan JPA? Apa kekhawatiran yang harus saya cari ketika memiliki EAGER muliple?
AR3Y35
6
masalahnya, hibernasi tidak dapat mengambil dua koleksi dengan satu permintaan. Jadi ketika Anda meminta entitas induk, itu akan membutuhkan 2 kueri tambahan per hasil, yang biasanya adalah sesuatu yang tidak Anda inginkan.
Bozho
7
Akan sangat bagus untuk memiliki penjelasan mengapa ini menyelesaikan masalah.
Webnet
290

Cukup ubah dari Listtipe ke Settipe.

Tetapi ingatkan bahwa Anda tidak akan menghilangkan Produk Cartesian yang mendasari sebagaimana dijelaskan oleh Vlad Mihalcea dalam jawabannya !

Ahmad Zyoud
sumber
42
Daftar dan Perangkat bukanlah hal yang sama: perangkat tidak mempertahankan ketertiban
Matteo
17
LinkedHashSet mempertahankan pesanan
egallardo
15
Ini adalah perbedaan penting dan, ketika Anda memikirkannya, sepenuhnya benar. Banyak-ke-satu khas yang diterapkan oleh kunci asing di DB benar-benar bukan Daftar, itu Set karena pesanan tidak dipertahankan. Jadi Set benar-benar lebih tepat. Saya pikir itu membuat perbedaan dalam hibernate, meskipun saya tidak tahu mengapa.
fool4jesus
3
Saya mengalami hal yang sama tidak dapat secara bersamaan mengambil beberapa tas tetapi bukan karena anotasi. Dalam kasus saya, saya melakukan gabungan kiri dan disjungsi dengan keduanya *ToMany. Mengubah tipe untuk Setmenyelesaikan masalah saya juga. Solusi yang sangat baik dan rapi. Ini harus menjadi jawaban resmi.
L. Holanda
20
Saya menyukai jawabannya, tetapi pertanyaan sejuta dolar adalah: Mengapa? Mengapa dengan Set tidak menunjukkan pengecualian? Terima kasih
Hinotori
140

Tambahkan anotasi @Fetch khusus Hibernate ke kode Anda:

@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;

Ini harus memperbaiki masalah, terkait dengan bug Hibernate HHH-1718

Dave Richardson
sumber
5
@DaveRlz mengapa subSelect memecahkan masalah ini. Saya mencoba solusi Anda dan berhasil, tetapi tidak tahu bagaimana masalah diselesaikan dengan menggunakan ini?
HakunaMatata
Ini adalah jawaban terbaik kecuali jika Setbenar - benar masuk akal. Memiliki OneToManyhubungan tunggal menggunakan Sethasil dalam 1+<# relationships>kueri, di mana seperti menggunakan FetchMode.SUBSELECThasil dalam 1+1kueri. Selain itu, menggunakan anotasi dalam jawaban yang diterima ( LazyCollectionOption.FALSE) menyebabkan lebih banyak kueri dieksekusi.
mstrthealias
1
FetchType.EAGER bukan solusi yang tepat untuk ini. Perlu melanjutkan dengan Hibernate Fetch Profiles dan perlu menyelesaikannya
Milinda Bandara
2
Dua jawaban teratas lainnya tidak menyelesaikan masalah saya. Yang ini. Terima kasih!
Blindworks
3
Adakah yang tahu mengapa SUBSELECT memperbaikinya, tetapi BERGABUNG tidak?
Innokenty
42

Pertanyaan ini telah menjadi tema yang berulang pada StackOverflow atau forum Hibernate, jadi saya memutuskan untuk mengubah jawabannya menjadi sebuah artikel juga.

Mengingat kami memiliki entitas berikut:

masukkan deskripsi gambar di sini

Dan, Anda ingin mengambil beberapa Postentitas induk beserta semua commentsdan tagskoleksi.

Jika Anda menggunakan lebih dari satu JOIN FETCHarahan:

List<Post> posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "left join fetch p.comments " +
    "left join fetch p.tags " +
    "where p.id between :minId and :maxId", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();

Hibernate akan melempar yang terkenal:

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags [
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.comments,
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.tags
]

Hibernate tidak memungkinkan mengambil lebih dari satu tas karena itu akan menghasilkan produk Cartesian .

"Solusi" terburuk

Sekarang, Anda akan menemukan banyak jawaban, posting blog, video, atau sumber daya lain yang memberi tahu Anda untuk menggunakan Setalih - alih Listuntuk koleksi Anda.

Itu saran yang mengerikan. Jangan lakukan itu!

Menggunakan Setsbukannya Listsakan membuat MultipleBagFetchExceptionpergi, tetapi Produk Cartesian masih akan ada, yang sebenarnya lebih buruk, karena Anda akan mengetahui masalah kinerja lama setelah Anda menerapkan "perbaikan" ini.

Solusi yang tepat

Anda dapat melakukan trik berikut:

List<Post> posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.comments " +
    "where p.id between :minId and :maxId ", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.tags t " +
    "where p in :posts ", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

Dalam permintaan JPQL pertama, distinctJANGAN pergi ke pernyataan SQL. Itu sebabnya kami menyetel PASS_DISTINCT_THROUGHpetunjuk kueri JPA ke false.

DISTINCT memiliki dua arti dalam JPQL, dan di sini, kita memerlukannya untuk mendeduplikasi referensi objek Java yang dikembalikan oleh getResultListdi sisi Java, bukan sisi SQL. Lihat artikel ini untuk lebih jelasnya.

Selama Anda mengambil paling banyak satu koleksi menggunakan JOIN FETCH, Anda akan baik-baik saja.

Dengan menggunakan beberapa kueri, Anda akan menghindari Produk Cartesian karena koleksi lain tetapi yang pertama diambil menggunakan kueri sekunder.

Masih banyak yang bisa Anda lakukan

Jika Anda menggunakan FetchType.EAGERstrategi pada waktu pemetaan untuk @OneToManyatau @ManyToManyasosiasi, maka Anda dapat dengan mudah berakhir dengan MultipleBagFetchException.

Anda lebih baik beralih dari FetchType.EAGERke Fetchype.LAZYkarena bersemangat mengambil adalah ide yang buruk yang dapat menyebabkan masalah kinerja aplikasi kritis .

Kesimpulan

Hindari FetchType.EAGERdan tidak beralih dari Listke Sethanya karena hal itu akan membuat Hibernate menyembunyikan MultipleBagFetchExceptionbawah karpet. Ambil hanya satu koleksi sekaligus, dan Anda akan baik-baik saja.

Selama Anda melakukannya dengan jumlah kueri yang sama seperti Anda memiliki koleksi untuk diinisialisasi, Anda baik-baik saja. Hanya saja, jangan menginisialisasi koleksi dalam satu lingkaran, karena itu akan memicu masalah permintaan N + 1 , yang juga buruk untuk kinerja.

Vlad Mihalcea
sumber
Terima kasih atas pengetahuannya. Namun, DISTINCTadalah pembunuh kinerja dalam solusi ini. Apakah ada cara untuk menyingkirkannya distinct? ( Set<...>Sebaliknya mencoba untuk kembali , tidak banyak membantu)
Leonid Dashko
1
DISTINCT tidak pergi ke pernyataan SQL. Itu sebabnya PASS_DISTINCT_THROUGHdiatur ke false. DISTINCT memiliki 2 arti dalam JPQL, dan di sini, kami membutuhkannya untuk dideduplikasi di sisi Java, bukan di sisi SQL. Lihat artikel ini untuk lebih jelasnya.
Vlad Mihalcea
Vlad, terima kasih atas bantuan yang menurut saya berguna. Namun, masalah ini terkait dengan hibernate.jdbc.fetch_size(akhirnya saya atur ke 350). Secara kebetulan, apakah Anda tahu cara mengoptimalkan hubungan bersarang? Misalnya entitas1 -> entitas2 -> entitas3.1, entitas 3.2 (di mana entitas3.1 / 3.2 adalah hubungan @OneToMany)
@OneToMany Leonid Dashko
1
@LeonidDashko Lihatlah bab Pengambilan di buku Java Persistence Kinerja Tinggi saya untuk banyak tips terkait dengan pengambilan data.
Vlad Mihalcea
1
Tidak Anda tidak bisa. Pikirkan tentang hal ini dalam hal SQL. Anda tidak dapat BERGABUNG dengan banyak asosiasi satu-ke-banyak tanpa menghasilkan Produk Cartesian.
Vlad Mihalcea
31

Setelah mencoba dengan setiap opsi yang dijelaskan dalam posting ini dan lainnya, saya sampai pada kesimpulan bahwa perbaikannya adalah sebagai berikut.

Di setiap tempat XToMany @ XXXToMany(mappedBy="parent", fetch=FetchType.EAGER) dan setelah menengah

@Fetch(value = FetchMode.SUBSELECT)

Ini berhasil untuk saya

Javier
sumber
5
menambahkan @Fetch(value = FetchMode.SUBSELECT)sudah cukup
user2601995
1
Ini adalah solusi Hibernate saja. Bagaimana jika Anda menggunakan perpustakaan JPA bersama?
Michel
3
Saya yakin Anda tidak bermaksud demikian, tetapi DaveRlz sudah menulis hal yang sama 3 tahun sebelumnya
phil294
21

Untuk memperbaikinya hanya mengambil Setdi tempat Listuntuk objek bersarang Anda.

@OneToMany
Set<Your_object> objectList;

dan jangan lupa gunakan fetch=FetchType.EAGER

ini akan bekerja.

Ada satu konsep lagi CollectionIddi Hibernate jika Anda ingin tetap menggunakan daftar saja.

Tetapi ingatkan bahwa Anda tidak akan menghilangkan Produk Cartesian yang mendasari sebagaimana dijelaskan oleh Vlad Mihalcea dalam jawabannya !

Prateek Singh
sumber
6

Anda dapat menyimpan daftar booth EAGER di JPA dan menambahkan setidaknya satu di antaranya anotasi JPA @OrderColumn (dengan jelas nama bidang yang akan dipesan). Tidak perlu penjelasan hibernasi khusus. Tetapi perlu diingat itu bisa membuat elemen kosong dalam daftar jika bidang yang dipilih tidak memiliki nilai mulai dari 0

 [...]
 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 @OrderColumn(name="orderIndex")
 private List<Child> children;
 [...]

pada Children maka Anda harus menambahkan bidang orderIndex

Massimo
sumber
2

Kami mencoba Set bukannya Daftar dan itu adalah mimpi buruk: ketika Anda menambahkan dua objek baru, equals () dan hashCode () gagal membedakan keduanya! Karena mereka tidak memiliki id.

alat khas seperti Eclipse menghasilkan kode semacam itu dari tabel Database:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
}

Anda juga dapat membaca artikel ini yang menjelaskan dengan benar seberapa kacau JPA / Hibernate. Setelah membaca ini, saya pikir ini adalah kali terakhir saya menggunakan ORM dalam hidup saya.

Saya juga bertemu dengan Domain Driven Design yang pada dasarnya mengatakan ORM adalah hal yang mengerikan.

Mike Dudley
sumber
1

Ketika Anda memiliki objek yang terlalu kompleks dengan koleksi saveral bukan ide yang baik untuk memiliki semuanya dengan EAGER fetchType, lebih baik gunakan LAZY dan ketika Anda benar-benar perlu memuat koleksi gunakan: Hibernate.initialize(parent.child)untuk mengambil data.

Felipe Cadena
sumber
0

Bagi saya, masalahnya adalah EAGER bersarang mengambil .

Salah satu solusinya adalah mengatur bidang bersarang ke LAZY dan menggunakan Hibernate.initialize () untuk memuat bidang bersarang:

x = session.get(ClassName.class, id);
Hibernate.initialize(x.getNestedField());
butter_prune
sumber
0

Pada akhirnya, ini terjadi ketika saya memiliki beberapa koleksi dengan FetchType.EAGER, seperti ini:

@ManyToMany(fetch = FetchType.EAGER, targetEntity = className.class)
@JoinColumn(name = "myClass_id")
@JsonView(SerializationView.Summary.class)
private Collection<Model> ModelObjects;

Selain itu, koleksi bergabung di kolom yang sama.

Untuk mengatasi masalah ini, saya mengubah salah satu koleksi menjadi FetchType.LAZY karena tidak masalah untuk use case saya.

Semoga berhasil! ~ J

JAD
sumber
0

Mengomentari keduanya Fetchdan LazyCollectionterkadang membantu menjalankan proyek.

@Fetch(FetchMode.JOIN)
@LazyCollection(LazyCollectionOption.FALSE)
prisar
sumber
0

Satu hal yang baik tentang itu @LazyCollection(LazyCollectionOption.FALSE)adalah bahwa beberapa bidang dengan anotasi ini dapat hidup berdampingan sementaraFetchType.EAGER tidak bisa, bahkan dalam situasi di mana koeksistensi seperti itu sah.

Sebagai contoh, suatu Ordermungkin memiliki daftar OrderGroup(yang pendek) serta daftar Promotions(juga pendek). @LazyCollection(LazyCollectionOption.FALSE)dapat digunakan pada keduanya tanpa menyebabkan LazyInitializationExceptionkeduanyaMultipleBagFetchException .

Dalam kasus saya @Fetchmemang memecahkan masalah saya MultipleBacFetchExceptiontetapi kemudian menyebabkan LazyInitializationException, no Sessionkesalahan terkenal .

WesternGun
sumber
-5

Anda dapat menggunakan anotasi baru untuk menyelesaikan ini:

@XXXToXXX(targetEntity = XXXX.class, fetch = FetchType.LAZY)

Bahkan, nilai default fetch adalah FetchType.LAZY juga.

leee41
sumber
5
JPA3.0 tidak ada.
holmis83