Hibernate Criteria ส่งคืนเด็กหลายครั้งด้วย FetchType.EAGER


115

ฉันมีOrderคลาสที่มีรายชื่อOrderTransactionsและฉันแมปด้วยการแมป Hibernate แบบหนึ่งต่อหลายดังนี้:

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

เหล่านี้Orderยังมีฟิลด์orderStatusซึ่งใช้สำหรับการกรองด้วยเกณฑ์ต่อไปนี้:

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

สิ่งนี้ได้ผลและผลลัพธ์ก็เป็นไปตามที่คาดหวัง

ตอนนี้ที่นี่เป็นคำถามของฉัน : ทำไมเมื่อฉันตั้งดึงข้อมูลประเภทอย่างชัดเจนที่จะEAGERทำOrders ปรากฏหลายครั้งในรายการผลลัพธ์?

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

ฉันจะต้องเปลี่ยนรหัสเกณฑ์อย่างไรเพื่อให้ได้ผลลัพธ์เดียวกันด้วยการตั้งค่าใหม่


1
คุณได้ลองเปิดใช้งาน show_sql เพื่อดูว่าเกิดอะไรขึ้นด้านล่างหรือไม่?
Mirko N.

กรุณาใส่รหัสคลาส OrderTransaction และ Order ด้วย \
Eran Medan

คำตอบ:


116

นี่เป็นพฤติกรรมที่คาดไว้หากฉันเข้าใจการกำหนดค่าของคุณอย่างถูกต้อง

คุณได้รับOrderอินสแตนซ์เดียวกันในผลลัพธ์ใด ๆ แต่เนื่องจากตอนนี้คุณกำลังทำการเข้าร่วมกับOrderTransactionมันจะต้องส่งคืนผลลัพธ์จำนวนเท่ากันการเข้าร่วม sql ปกติจะกลับมา

ดังนั้นที่จริงแล้วมันควรจะปรากฏหลายครั้ง สิ่งนี้อธิบายได้เป็นอย่างดีโดยผู้เขียน (Gavin King) เองที่นี่ : ทั้งสองอธิบายเหตุผลและวิธีการยังคงได้ผลลัพธ์ที่แตกต่างกัน


นอกจากนี้ยังกล่าวถึงในคำถามที่พบบ่อยของไฮเบอร์เนต:

ไฮเบอร์เนตไม่ส่งคืนผลลัพธ์ที่แตกต่างกันสำหรับแบบสอบถามที่เปิดใช้การดึงการรวมภายนอกสำหรับคอลเล็กชัน (แม้ว่าฉันจะใช้คีย์เวิร์ดที่แตกต่างกันก็ตาม)? ขั้นแรกคุณต้องเข้าใจ SQL และวิธีการทำงานของ OUTER JOIN ใน SQL หากคุณไม่เข้าใจและเข้าใจการรวมภายนอกใน SQL อย่างถ่องแท้อย่าอ่านรายการคำถามที่พบบ่อยนี้ต่อไป แต่ให้ศึกษาคู่มือหรือบทช่วยสอนเกี่ยวกับ SQL มิฉะนั้นคุณจะไม่เข้าใจคำอธิบายต่อไปนี้และคุณจะบ่นเกี่ยวกับพฤติกรรมนี้ในฟอรัม Hibernate

ตัวอย่างทั่วไปที่อาจส่งคืนการอ้างอิงซ้ำของอ็อบเจ็กต์ Order เดียวกัน:

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

ตัวอย่างทั้งหมดเหล่านี้สร้างคำสั่ง SQL เดียวกัน:

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

ต้องการทราบว่าเหตุใดจึงมีรายการที่ซ้ำกัน ดูที่ชุดผลลัพธ์ SQL ไฮเบอร์เนตไม่ได้ซ่อนรายการที่ซ้ำกันเหล่านี้ทางด้านซ้ายของผลการรวมภายนอก แต่ส่งกลับรายการที่ซ้ำกันทั้งหมดของตารางการขับขี่ หากคุณมีคำสั่งซื้อ 5 รายการในฐานข้อมูลและแต่ละคำสั่งมี 3 รายการโฆษณาชุดผลลัพธ์จะเป็น 15 แถว รายการผลลัพธ์ Java ของแบบสอบถามเหล่านี้จะมี 15 องค์ประกอบทุกประเภทคำสั่ง Hibernate จะสร้างอินสแตนซ์คำสั่งเพียง 5 อินสแตนซ์ แต่รายการที่ซ้ำกันของชุดผลลัพธ์ SQL จะถูกเก็บไว้เป็นการอ้างอิงซ้ำกับอินสแตนซ์ทั้ง 5 นี้ หากคุณไม่เข้าใจประโยคสุดท้ายนี้คุณต้องอ่าน Java และความแตกต่างระหว่างอินสแตนซ์บนฮีป Java และการอ้างอิงถึงอินสแตนซ์ดังกล่าว

(ทำไมต้องเข้าร่วมด้านนอกด้านซ้ายหากคุณมีคำสั่งซื้อเพิ่มเติมโดยไม่มีรายการโฆษณาชุดผลลัพธ์จะเป็น 16 แถวโดยเติม NULL ทางด้านขวาโดยที่ข้อมูลรายการโฆษณาเป็นข้อมูลสำหรับคำสั่งซื้ออื่น ๆ คุณต้องการคำสั่งซื้อแม้ว่า พวกเขาไม่มีรายการโฆษณาใช่ไหมถ้าไม่มีให้ใช้การดึงการรวมภายในใน HQL ของคุณ)

ไฮเบอร์เนตไม่กรองข้อมูลอ้างอิงที่ซ้ำกันเหล่านี้ออกไปโดยค่าเริ่มต้น บางคน (ไม่ใช่คุณ) ต้องการสิ่งนี้จริงๆ คุณจะกรองออกได้อย่างไร?

แบบนี้:

Collection result = new LinkedHashSet( session.create*(...).list() );

122
แม้ว่าคุณจะเข้าใจคำอธิบายต่อไปนี้ แต่คุณอาจบ่นเกี่ยวกับพฤติกรรมนี้ในฟอรัม Hibernate เพราะมันเป็นการพลิกพฤติกรรมโง่ ๆ !
Tom Anderson

17
ค่อนข้างถูกต้องทอม Id ลืมเกี่ยวกับทัศนคติที่หยิ่งผยองของ Gavin Kings นอกจากนี้เขายังกล่าวว่า 'ไฮเบอร์เนตไม่ได้กรองการอ้างอิงที่ซ้ำกันเหล่านี้ออกไปโดยค่าเริ่มต้น บางคน (ไม่ใช่คุณ) ต้องการสิ่งนี้จริงๆ 'สนใจเมื่อผู้คนต่อต้านสิ่งนี้จริงๆ
Paul Taylor

16
@ TomAnderson ใช่แล้ว ทำไมทุกคนถึงต้องการข้อมูลซ้ำเหล่านั้น ฉันกำลังถามด้วยความอยากรู้อยากเห็นอย่างแท้จริงเนื่องจากฉันไม่รู้ ... คุณสามารถสร้างรายการที่ซ้ำกันได้ด้วยตัวเองมากเท่าที่คุณต้องการ .. ;-)
Parobay

13
ถอนหายใจ นี่เป็นข้อบกพร่องของ Hibernate IMHO ฉันต้องการเพิ่มประสิทธิภาพการค้นหาของฉันฉันจึงเริ่มจาก "เลือก" เป็น "เข้าร่วม" ในไฟล์การแมปของฉัน ทันใดนั้นรหัสของฉันก็หยุดไปทั่วทุกที่ จากนั้นฉันก็วิ่งไปรอบ ๆ และแก้ไข DAO ทั้งหมดของฉันโดยการต่อท้ายตัวแปลงผลลัพธ์และอะไรก็ตาม ประสบการณ์ของผู้ใช้ == เชิงลบมาก ฉันเข้าใจว่าบางคนชอบที่จะมีรายการซ้ำด้วยเหตุผลแปลก ๆ แต่ทำไมฉันไม่พูดว่า "ดึงข้อมูลวัตถุเหล่านี้ให้เร็วขึ้น แต่อย่าทำให้ฉันมีรายการซ้ำ" โดยระบุ fetch = "justworkplease"
Roman Zenka

@Eran: ฉันกำลังเจอปัญหาคล้าย ๆ กัน ฉันไม่ได้รับอ็อบเจ็กต์พาเรนต์ที่ซ้ำกัน แต่ฉันได้รับเด็กในแต่ละอ็อบเจ็กต์พาเรนต์ซ้ำหลาย ๆ ครั้งเนื่องจากมีอ็อบเจ็กต์พาเรนต์จำนวนมากในการตอบสนอง มีความคิดว่าทำไมปัญหานี้?
mantri

93

นอกเหนือจากสิ่งที่ Eran กล่าวถึงแล้วอีกวิธีหนึ่งในการรับพฤติกรรมที่คุณต้องการคือการตั้งค่าหม้อแปลงผลลัพธ์:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

8
สิ่งนี้จะใช้ได้กับกรณีส่วนใหญ่ .... ยกเว้นเมื่อคุณพยายามใช้เกณฑ์เพื่อดึงข้อมูลคอลเลกชัน / การเชื่อมโยง 2 รายการ
JamesD

42

ลอง

@Fetch (FetchMode.SELECT) 

ตัวอย่างเช่น

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

}


11
FetchMode.SELECT เพิ่มจำนวนคิวรี SQL ที่ Hibernate ทำงาน แต่ทำให้มั่นใจได้เพียงหนึ่งอินสแตนซ์ต่อเรกคอร์ดเอนทิตีราก ไฮเบอร์เนตจะเริ่มการทำงานที่เลือกสำหรับบันทึกย่อยทุกรายการในกรณีนี้ ดังนั้นคุณควรพิจารณาเกี่ยวกับการพิจารณาประสิทธิภาพ
Bipul

1
@BipulKumar ใช่ แต่นี่เป็นตัวเลือกเมื่อเราไม่สามารถใช้การดึงข้อมูลแบบขี้เกียจได้เนื่องจากเราต้องรักษาเซสชันสำหรับการดึงข้อมูลแบบขี้เกียจเพื่อเข้าถึงวัตถุย่อย
mathi

18

อย่าใช้ List และ ArrayList แต่ตั้งค่าและ HashSet

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

2
นี่เป็นการกล่าวถึงแนวทางปฏิบัติที่ดีที่สุดของ Hibernate โดยบังเอิญหรือเกี่ยวข้องกับคำถามที่เรียกข้อมูลเด็กหลายคนจาก OP หรือไม่
Jacob Zwiers


เข้าใจแล้ว รองจากคำถามของ OP แม้ว่าบทความ dzone น่าจะนำมาด้วยเม็ดเกลือ ... ตามการยอมรับของผู้เขียนเองในความคิดเห็น
Jacob Zwiers

2
นี่เป็น IMO ที่ตอบโจทย์ได้ดีมาก หากคุณไม่ต้องการให้ซ้ำกันมีความเป็นไปได้สูงที่คุณจะใช้ชุดมากกว่ารายการ - การใช้ชุด (และใช้วิธีการเท่ากับ / แฮชโค้ดที่ถูกต้องแน่นอน) ช่วยแก้ปัญหาให้ฉันได้ ระวังเมื่อใช้ hashcode / equals ตามที่ระบุไว้ใน redhat doc อย่าใช้ฟิลด์ id
จ้า

1
ขอบคุณสำหรับ IMO ของคุณ นอกจากนี้อย่ามีปัญหาในการสร้างเมธอด equals () และ hashCode () ให้ IDE หรือลอมบอกของคุณสร้างให้คุณ
Αλέκος

3

การใช้ Java 8 และ Streams ฉันเพิ่มในวิธียูทิลิตี้ของฉันสิ่งนี้ส่งคืน statment:

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

สตรีมลบรายการที่ซ้ำกันเร็วมาก ฉันใช้คำอธิบายประกอบในคลาสเอนทิตีของฉันดังนี้:

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

ฉันคิดว่าดีกว่าในแอปของฉันที่จะใช้เซสชันในวิธีที่ฉันต้องการข้อมูลจากฐานข้อมูล ปิดเซสชั่นเมื่อฉันทำเสร็จ Ofcourse ตั้งค่าคลาสเอนทิตีของฉันให้ใช้ประเภทการดึงข้อมูลแบบง่าย ฉันไปที่ refactor


3

ฉันมีปัญหาเดียวกันในการเรียก 2 คอลเล็กชันที่เกี่ยวข้อง: ผู้ใช้มี 2 บทบาท (ชุด) และ 2 มื้อ (รายการ) และอาหารซ้ำกัน

@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 ไม่ช่วย (แบบสอบถาม 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);

ในที่สุดฉันพบ 2 วิธีแก้ไข:

  1. เปลี่ยนรายการเป็น LinkedHashSet
  2. ใช้ EntityGraph กับฟิลด์ "meal" เท่านั้นและพิมพ์ LOAD ซึ่งโหลดบทบาทตามที่ประกาศไว้ (EAGER และตาม BatchSize = 200 เพื่อป้องกันปัญหา N + 1):

ทางออกสุดท้าย:

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

1

แทนที่จะใช้แฮ็กเช่น:

  • Set แทน List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

ซึ่งไม่ได้แก้ไขแบบสอบถาม sql ของคุณเราสามารถใช้ได้ (อ้างถึงข้อกำหนด JPA)

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

ซึ่งจะแก้ไขแบบสอบถาม sql ที่เป็นผลลัพธ์ดังนั้นจึงมีDISTINCTอยู่ในนั้น


0

ฟังดูไม่ใช่พฤติกรรมที่ยอดเยี่ยมในการใช้การรวมภายนอกและทำให้เกิดผลลัพธ์ที่ซ้ำ ทางออกเดียวที่เหลือคือกรองผลลัพธ์ของเราโดยใช้สตรีม ขอบคุณ java8 ที่ให้วิธีที่ง่ายกว่าในการกรอง

return results.stream().distinct().collect(Collectors.toList());
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.