Kriteria Hibernasi mengembalikan anak beberapa kali dengan FetchType.EAGER

115

Saya memiliki Orderkelas yang memiliki daftar OrderTransactionsdan saya memetakannya dengan pemetaan Hibernate satu-ke-banyak seperti:

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Ini Orderjuga memiliki bidang orderStatus, yang digunakan untuk memfilter dengan Kriteria berikut:

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

Ini berhasil dan hasilnya seperti yang diharapkan.

Sekarang inilah pertanyaan saya : Mengapa, ketika saya menyetel jenis pengambilan secara eksplisit ke EAGER, apakah Ordermuncul beberapa kali dalam daftar yang dihasilkan?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

Bagaimana saya harus mengubah kode Kriteria saya untuk mencapai hasil yang sama dengan pengaturan baru?

raoulsson
sumber
1
Sudahkah Anda mencoba mengaktifkan show_sql untuk melihat apa yang terjadi di bawahnya?
Mirko N.
Silakan tambahkan kode OrderTransaction dan kelas Order juga. \
Eran Medan

Jawaban:

115

Ini sebenarnya adalah perilaku yang diharapkan jika saya memahami konfigurasi Anda dengan benar.

Anda mendapatkan Ordercontoh yang sama di salah satu hasil, tetapi karena sekarang Anda melakukan penggabungan dengan OrderTransaction, itu harus mengembalikan jumlah hasil yang sama dengan gabungan sql biasa akan mengembalikan

Jadi sebenarnya itu harus muncul beberapa kali. ini dijelaskan dengan sangat baik oleh penulis (Gavin King) sendiri di sini : Keduanya menjelaskan mengapa, dan bagaimana tetap mendapatkan hasil yang berbeda


Juga disebutkan di FAQ Hibernate :

Hibernate tidak mengembalikan hasil yang berbeda untuk kueri dengan pengambilan gabungan luar diaktifkan untuk koleksi (meskipun saya menggunakan kata kunci yang berbeda)? Pertama, Anda perlu memahami SQL dan cara kerja OUTER JOIN di SQL. Jika Anda tidak sepenuhnya memahami dan memahami gabungan luar dalam SQL, jangan lanjutkan membaca item FAQ ini tetapi lihat manual atau tutorial SQL. Jika tidak, Anda tidak akan memahami penjelasan berikut dan Anda akan mengeluh tentang perilaku ini di forum Hibernate.

Contoh umum yang mungkin mengembalikan referensi duplikat dari objek Order yang sama:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

Semua contoh ini menghasilkan pernyataan SQL yang sama:

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

Ingin tahu mengapa duplikatnya ada? Lihatlah set hasil SQL, Hibernate tidak menyembunyikan duplikat ini di sisi kiri hasil gabungan luar tetapi mengembalikan semua duplikat dari tabel penggerak. Jika Anda memiliki 5 pesanan dalam database, dan setiap pesanan memiliki 3 item baris, kumpulan hasil akan menjadi 15 baris. Daftar hasil Java dari query ini akan memiliki 15 elemen, semua bertipe Order. Hanya 5 instance Order yang akan dibuat oleh Hibernate, tetapi duplikat dari kumpulan hasil SQL disimpan sebagai referensi duplikat untuk 5 instance ini. Jika Anda tidak memahami kalimat terakhir ini, Anda perlu membaca tentang Java dan perbedaan antara instance di heap Java dan referensi ke instance semacam itu.

(Mengapa left outer join? Jika Anda memiliki pesanan tambahan tanpa item baris, kumpulan hasil akan menjadi 16 baris dengan NULL mengisi sisi kanan, di mana data item baris untuk pesanan lain. Anda menginginkan pesanan meskipun mereka tidak memiliki item baris, bukan? Jika tidak, gunakan inner join fetch di HQL Anda).

Hibernate tidak memfilter referensi duplikat ini secara default. Beberapa orang (bukan Anda) sebenarnya menginginkan ini. Bagaimana Anda bisa menyaringnya?

Seperti ini:

Collection result = new LinkedHashSet( session.create*(...).list() );
Eran Medan
sumber
121
Bahkan jika Anda memahami penjelasan berikut, Anda mungkin akan mengeluh tentang perilaku ini di forum Hibernate, karena itu adalah perilaku bodoh yang membalik!
Tom Anderson
17
Benar sekali Tom, aku sudah melupakan sikap arogan Gavin Kings. Dia juga mengatakan 'Hibernate tidak menyaring referensi duplikat ini secara default. Beberapa orang (bukan Anda) benar-benar ingin ini 'Saya tertarik ketika orang benar-benar menentang ini.
Paul Taylor
16
@TomAnderson ya persis. mengapa ada orang yang membutuhkan duplikat itu? Saya bertanya karena ingin tahu, karena saya tidak tahu ... Anda dapat membuat duplikat sendiri, sebanyak yang Anda inginkan .. ;-)
Parobay
13
Mendesah. Ini sebenarnya cacat Hibernate, IMHO. Saya ingin mengoptimalkan kueri saya, jadi saya beralih dari "pilih" menjadi "bergabung" dalam file pemetaan saya. Tiba-tiba kode saya BREAKS di semua tempat. Kemudian saya berkeliling dan memperbaiki semua DAO saya dengan menambahkan transformator hasil dan yang lainnya. Pengalaman pengguna == sangat negatif. Saya memahami bahwa beberapa orang benar-benar suka memiliki duplikat karena alasan yang aneh, tetapi mengapa saya tidak dapat mengatakan "ambil objek ini LEBIH CEPAT tetapi jangan ganggu saya dengan duplikat" dengan menentukan fetch = "justworkplease"?
Roman Zenka
@Eran: Saya menghadapi masalah serupa. Saya tidak mendapatkan objek induk duplikat, tetapi saya mendapatkan anak di setiap objek induk diulang sebanyak jumlah objek induk dalam respons. Tahu mengapa masalah ini?
mantri
93

Selain apa yang disebutkan oleh Eran, cara lain untuk mendapatkan perilaku yang Anda inginkan, adalah dengan mengatur transformator hasil:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
EJB
sumber
8
Ini akan bekerja untuk kebanyakan kasus .... kecuali ketika Anda mencoba menggunakan Kriteria untuk mengambil 2 koleksi / asosiasi.
JamesD
42

mencoba

@Fetch (FetchMode.SELECT) 

sebagai contoh

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}

mathi
sumber
11
FetchMode.SELECT meningkatkan jumlah kueri SQL yang dijalankan oleh Hibernate tetapi memastikan hanya satu contoh per catatan entitas akar. Hibernate akan mengaktifkan pilihan untuk setiap rekaman anak dalam kasus ini. Jadi, Anda harus memperhitungkannya sehubungan dengan pertimbangan kinerja.
Bipul
1
@BipulKumar ya, tetapi ini adalah opsi ketika kita tidak dapat menggunakan pengambilan malas karena kita perlu mempertahankan sesi pengambilan lambat untuk mengakses sub objek.
mathi
18

Jangan gunakan List dan ArrayList tapi Set dan HashSet.

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}
Αλέκος
sumber
2
Apakah ini penyebutan insidental tentang praktik terbaik Hibernate atau relevan dengan pertanyaan pengambilan ulang multi-anak dari OP?
Jacob Zwiers
Mengerti. Sekunder dari pertanyaan OP. Padahal, artikel dzone mungkin harus diambil dengan sebutir garam ... berdasarkan pengakuan penulis sendiri di komentar.
Jacob Zwiers
2
Ini adalah jawaban yang sangat bagus IMO. Jika Anda tidak ingin duplikat, sangat mungkin Anda lebih suka menggunakan Set daripada Daftar- Menggunakan Set (dan menerapkan metode sama / hascode yang benar tentu saja) memecahkan masalah bagi saya. Berhati-hatilah saat menerapkan kode hash / sama dengan, seperti yang dinyatakan di dokumen redhat, jangan gunakan kolom id.
Mat
1
Terima kasih atas IMO Anda. Selain itu, jangan kesulitan membuat metode equals () dan hashCode (). Biarkan IDE Anda atau Lombok membuatnya untuk Anda.
Αλέκος
3

Menggunakan Java 8 dan Streams, saya menambahkan metode utilitas saya, pernyataan pengembalian ini:

return results.stream().distinct().collect(Collectors.toList());

Stream menghapus duplikat dengan sangat cepat. Saya menggunakan anotasi di kelas Entitas saya seperti ini:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

Saya pikir lebih baik di aplikasi saya untuk menggunakan sesi dalam metode di mana saya membutuhkan data dari basis data. Tutup sesi ketika saya selesai. Tentu saja menyetel kelas Entitas saya untuk menggunakan jenis pengambilan yang disewakan. Saya pergi ke refactor.

easyScript
sumber
3

Saya memiliki masalah yang sama untuk mengambil 2 koleksi terkait: pengguna memiliki 2 peran (Set) dan 2 makanan (Daftar) dan makanan digandakan.

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCT tidak membantu (kueri DATA-JPA):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

Akhirnya saya menemukan 2 solusi:

  1. Ubah Daftar ke LinkedHashSet
  2. Gunakan EntityGraph hanya dengan kolom "meal" dan ketik LOAD, yang memuat peran seperti yang dideklarasikan (EAGER dan BatchSize = 200 untuk mencegah masalah N + 1):

Solusi akhir:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);
Grigory Kislin
sumber
1

Daripada menggunakan peretasan seperti:

  • Set dari pada List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

yang tidak mengubah kueri sql Anda, kami dapat menggunakan (mengutip spesifikasi JPA)

q.select(emp).distinct(true);

yang memang mengubah kueri sql yang dihasilkan, sehingga memiliki a DISTINCTdi dalamnya.

Kaustubh
sumber
0

Kedengarannya bukan perilaku yang bagus dengan menerapkan gabungan luar dan membawa hasil duplikat. Satu-satunya solusi yang tersisa adalah memfilter hasil kami menggunakan aliran. Terima kasih java8 memberikan cara yang lebih mudah untuk memfilter.

return results.stream().distinct().collect(Collectors.toList());
Pravin
sumber