JPA hashCode () / เท่ากับ () ขึ้นเขียง


311

มีการบาง อภิปรายนี่เกี่ยวกับหน่วยงานและที่ JPA hashCode()/ equals()การดำเนินงานควรจะใช้สำหรับการเรียน JPA นิติบุคคล ส่วนใหญ่ (ถ้าไม่ใช่ทั้งหมด) ขึ้นอยู่กับ Hibernate แต่ฉันต้องการจะพูดคุยเกี่ยวกับ JPA-Implement-neutrally (ฉันใช้ EclipseLink)

การนำไปใช้งานที่เป็นไปได้ทั้งหมดนั้นมีข้อดีและข้อเสียของตัวเองเกี่ยวกับ:

  • hashCode()/ ความequals()สอดคล้องตามสัญญา(ไม่เปลี่ยนแปลง) สำหรับList/ Setการดำเนินงาน
  • ไม่ว่าจะเป็นวัตถุที่เหมือนกัน (เช่นจากเซสชันที่แตกต่างกันพร็อกซีไดนามิกจากโครงสร้างข้อมูลที่โหลดแบบ lazily) สามารถตรวจพบได้
  • ไม่ว่าเอนทิตีจะทำงานอย่างถูกต้องในสถานะแยกออก (หรือไม่คงอยู่)

เท่าที่ฉันเห็นมีสามตัวเลือก :

  1. อย่าแทนที่พวกเขา; พึ่งพาObject.equals()และObject.hashCode()
    • hashCode()/ equals()งาน
    • ไม่สามารถระบุวัตถุที่เหมือนกันปัญหาเกี่ยวกับพร็อกซีแบบไดนามิก
    • ไม่มีปัญหากับเอนทิตี้เดี่ยว
  2. แทนที่พวกเขาขึ้นอยู่กับคีย์หลัก
    • hashCode()/ equals()ถูกทำลาย
    • ตัวตนที่ถูกต้อง (สำหรับองค์กรที่มีการจัดการทั้งหมด)
    • ปัญหาเกี่ยวกับหน่วยงานเดี่ยว
  3. แทนที่พวกเขาตามBusiness-Id (เขตข้อมูลคีย์ที่ไม่ใช่หลัก; สิ่งที่เกี่ยวกับกุญแจต่างประเทศ?)
    • hashCode()/ equals()ถูกทำลาย
    • ตัวตนที่ถูกต้อง (สำหรับองค์กรที่มีการจัดการทั้งหมด)
    • ไม่มีปัญหากับเอนทิตี้เดี่ยว

คำถามของฉันคือ:

  1. ฉันพลาดตัวเลือกและ / หรือจุดโปรหรือไม่
  2. คุณเลือกตัวเลือกอะไรและทำไม?



อัปเดต 1:

โดย " hashCode()/ equals()จะแตก" ฉันหมายความว่าเนื่องhashCode()สวดอาจจะกลับค่าที่แตกต่างกันซึ่งเป็น (เมื่อใช้อย่างถูกต้อง) ไม่เสียในแง่ของObjectเอกสาร API แต่ที่ทำให้เกิดปัญหาเมื่อพยายามที่จะดึงการเปลี่ยนแปลงนิติบุคคลจากMap, Setหรืออื่น ๆ Collectionกัญชาตาม ดังนั้นการปรับใช้ JPA (อย่างน้อย EclipseLink) จะไม่ทำงานอย่างถูกต้องในบางกรณี

อัปเดต 2:

ขอบคุณสำหรับคำตอบ - ส่วนใหญ่มีคุณภาพที่น่าทึ่ง
น่าเสียดายที่ฉันยังคงไม่แน่ใจว่าวิธีการใดที่จะดีที่สุดสำหรับแอปพลิเคชันในชีวิตจริงหรือวิธีกำหนดวิธีที่ดีที่สุดสำหรับแอปพลิเคชันของฉัน ดังนั้นฉันจะเปิดคำถามและหวังว่าจะได้มีการพูดคุยและ / หรือความคิดเห็นเพิ่มเติม


4
ฉันไม่เข้าใจสิ่งที่คุณหมายถึงโดย "hashCode () / เท่ากับ () เสีย"
nanda

4
พวกเขาจะไม่ "แตก" ในแง่นั้นดังนั้นในตัวเลือกที่ 2 และ 3 คุณจะใช้ทั้งสองเท่ากับ () และ hashCode () โดยใช้กลยุทธ์เดียวกัน
matt b

11
นั่นไม่เป็นความจริงของตัวเลือก 3 hashCode () และ equals () ควรใช้เกณฑ์เดียวกันดังนั้นหากหนึ่งในฟิลด์ของคุณเปลี่ยนใช่วิธี hashcode () จะส่งคืนค่าที่แตกต่างกันสำหรับอินสแตนซ์เดียวกันก่อนหน้านี้ แต่จะเท่ากับ () คุณออกจากส่วนที่สองของประโยคจาก hashcode () javadoc: เมื่อใดก็ตามที่มีการเรียกใช้บนวัตถุเดียวกันมากกว่าหนึ่งครั้งในระหว่างการดำเนินการของแอปพลิเคชัน Java เมธอด hashCode จะต้องส่งคืนจำนวนเต็มเดียวกันโดยไม่มีข้อมูล ที่ใช้ในการเปรียบเทียบเท่ากับบนวัตถุที่มีการแก้ไข
matt b

1
จริง ๆ แล้วส่วนหนึ่งของประโยคหมายถึงการตรงกันข้าม - การเรียกhashcode()ใช้อินสแตนซ์วัตถุเดียวกันควรส่งคืนค่าเดียวกันเว้นแต่ว่าฟิลด์ใด ๆ ที่ใช้ในequals()การเปลี่ยนแปลงการใช้งาน กล่าวอีกนัยหนึ่งถ้าคุณมีสามเขตข้อมูลในชั้นเรียนของคุณและequals()วิธีการของคุณใช้เพียงสองเขตข้อมูลเพื่อกำหนดความเท่าเทียมกันของอินสแตนซ์คุณสามารถคาดหวังว่าhashcode()ค่าส่งคืนจะเปลี่ยนไปถ้าคุณเปลี่ยนค่าใดค่าหนึ่งของเขตข้อมูลเหล่านั้น ที่อินสแตนซ์วัตถุนี้จะไม่ "เท่ากับ" กับค่าที่เป็นตัวแทนอินสแตนซ์เก่าอีกต่อไป
matt b

2
"ปัญหาเมื่อพยายามดึงเอนทิตีที่ถูกเปลี่ยนจากแผนที่ชุดหรือคอลเลกชั่นที่ใช้แฮช" ... นี่ควรเป็น "ปัญหาเมื่อพยายามดึงเอนทิตีที่ถูกเปลี่ยนจาก HashMap, HashSet หรือคอลเลกชั่นอื่น ๆ ที่ใช้แฮช"
nanda

คำตอบ:


122

อ่านบทความนี้ดีมากในเรื่อง: อย่าปล่อยให้ Hibernate ขโมยเอกลักษณ์ของคุณ

บทสรุปของบทความเป็นดังนี้:

ตัวตนของวัตถุนั้นยากที่จะนำไปใช้อย่างถูกต้องเมื่อวัตถุนั้นยังคงอยู่ในฐานข้อมูล อย่างไรก็ตามปัญหาเกิดจากการอนุญาตให้วัตถุมีอยู่โดยไม่มีรหัสก่อนที่พวกเขาจะถูกบันทึกไว้ เราสามารถแก้ปัญหาเหล่านี้ได้โดยการรับผิดชอบในการกำหนด ID วัตถุให้พ้นจากกรอบการแมปเชิงวัตถุเช่นไฮเบอร์เนต แต่สามารถกำหนด ID ของวัตถุได้ทันทีที่วัตถุนั้นได้รับอินสแตนซ์ ทำให้ตัวตนของวัตถุง่ายและปราศจากข้อผิดพลาดและลดจำนวนของรหัสที่จำเป็นในรูปแบบโดเมน


21
ไม่นั่นไม่ใช่บทความที่ดี นั่นเป็นบทความที่ยอดเยี่ยมเกี่ยวกับเรื่องนี้และควรจะต้องอ่านสำหรับโปรแกรมเมอร์ JPA ทุกคน! +1!
Tom Anderson

2
ใช่ฉันใช้โซลูชันเดียวกัน การไม่ปล่อยให้ฐานข้อมูลสร้าง ID นั้นมีข้อดีอื่นเช่นกันเช่นความสามารถในการสร้างวัตถุและสร้างวัตถุอื่น ๆ ที่อ้างอิงก่อนที่จะคงอยู่ สิ่งนี้สามารถลบ latency และรอบคำขอ / ตอบกลับหลายรอบในแอปไคลเอ็นต์เซิร์ฟเวอร์ หากคุณต้องการแรงบันดาลใจสำหรับการแก้ปัญหาดังกล่าวให้ตรวจสอบโครงการของฉัน: suid.jsและร็เซิร์ฟเวอร์ Java โดยทั่วไปจะsuid.jsดึงข้อมูล ID บล็อกsuid-server-javaที่คุณสามารถรับและใช้ฝั่งไคลเอ็นต์
Stijn de Witt

2
นี่มันบ้าไปแล้ว ฉันยังใหม่ต่อการจำศีลภายใต้ประทุนเขียนการทดสอบหน่วยและพบว่าฉันไม่สามารถลบวัตถุออกจากชุดหลังจากแก้ไขได้สรุปว่ามันเป็นเพราะการเปลี่ยนแปลง hashcode แต่ไม่เข้าใจ เพื่อแก้ปัญหา บทความนี้เรียบง่ายงดงาม!
XM

มันเป็นบทความที่ยอดเยี่ยม อย่างไรก็ตามสำหรับผู้ที่เห็นลิงค์เป็นครั้งแรกฉันขอแนะนำว่าอาจเป็นการ overkill สำหรับแอปพลิเคชันส่วนใหญ่ ตัวเลือกอื่น ๆ 3 รายการที่ปรากฏในหน้านี้จะช่วยแก้ปัญหาได้หลายวิธี
HopeKing

1
Hibernate / JPA ใช้วิธีการเท่ากับและ hashcode ของเอนทิตีเพื่อตรวจสอบว่าบันทึกมีอยู่แล้วในฐานข้อมูลหรือไม่?
Tushar Banne

64

ฉันมักจะแทนที่เท่ากับ / hashcode และใช้มันขึ้นอยู่กับรหัสธุรกิจ ดูเหมือนทางออกที่สมเหตุสมผลที่สุดสำหรับฉัน ดูต่อไปนี้การเชื่อมโยง

เพื่อสรุปสิ่งทั้งหมดนี้นี่คือรายชื่อของสิ่งที่จะทำงานหรือจะไม่ทำงานด้วยวิธีต่างๆในการจัดการเท่ากับ / hashCode: ป้อนคำอธิบายรูปภาพที่นี่

แก้ไข :

เพื่ออธิบายว่าทำไมสิ่งนี้ถึงได้ผลสำหรับฉัน:

  1. ฉันมักจะไม่ใช้คอลเล็กชันที่ใช้แฮช (HashMap / HashSet) ในแอปพลิเคชัน JPA ของฉัน ถ้าฉันต้องฉันต้องการสร้างโซลูชัน UniqueList
  2. ฉันคิดว่าการเปลี่ยนรหัสธุรกิจบนรันไทม์ไม่ใช่วิธีที่ดีที่สุดสำหรับแอปพลิเคชันฐานข้อมูลใด ๆ ในกรณีที่หายากซึ่งไม่มีวิธีแก้ปัญหาอื่นฉันจะทำการดูแลเป็นพิเศษเช่นลบองค์ประกอบแล้วนำกลับไปที่คอลเล็กชั่นที่แฮช
  3. สำหรับโมเดลของฉันฉันตั้ง id ธุรกิจบนนวกรรมิกและไม่ได้ตั้งค่าให้มัน ฉันอนุญาตให้ JPA นำไปใช้เพื่อเปลี่ยนฟิลด์แทนคุณสมบัติ
  4. ดูเหมือนว่าโซลูชันของ UUID จะ overkill มากเกินไป ทำไมต้อง UUID ถ้าคุณมี ID ธุรกิจตามธรรมชาติ ฉันจะกำหนดเอกลักษณ์ของรหัสธุรกิจในฐานข้อมูล เหตุใดจึงมีดัชนีสามรายการสำหรับแต่ละตารางในฐานข้อมูล

1
แต่ตารางนี้ขาดบรรทัดที่ห้า "ทำงานกับรายการ / ชุด" (ถ้าคุณคิดว่าจะลบเอนทิตีซึ่งเป็นส่วนหนึ่งของชุดจากการแมป OneToMany) ซึ่งจะตอบว่า "ไม่" ในสองตัวเลือกสุดท้ายเนื่องจาก hashCode ( ) การเปลี่ยนแปลงที่ละเมิดสัญญา
MRalwasser

ดูความคิดเห็นเกี่ยวกับคำถาม คุณดูเหมือนจะเข้าใจผิดสัญญาเท่ากับ / hashcode
nanda

1
@Malwasser: ฉันคิดว่าคุณหมายถึงสิ่งที่ถูกต้องมันไม่ใช่แค่สัญญา / hashCode () ที่ทำผิดสัญญา แต่ค่าที่ไม่แน่นอนเท่ากับ / hashCode จะสร้างปัญหากับชุดสัญญา
Chris Lercher

3
@MRalwasser: แฮชโค้ดสามารถเปลี่ยนแปลงได้หากรหัสธุรกิจเปลี่ยนไปและประเด็นก็คือรหัสธุรกิจจะไม่เปลี่ยนแปลง ดังนั้นรหัสแฮชจะไม่เปลี่ยนแปลงและสามารถใช้งานได้อย่างสมบูรณ์กับคอลเลกชันที่แฮช
Tom Anderson

1
ถ้าคุณไม่มีรหัสธุรกิจที่เป็นธรรมชาติ ตัวอย่างเช่นในกรณีของจุดสองมิติ Point (X, Y) ในแอปพลิเคชันการวาดกราฟ? คุณจะเก็บคะแนนนั้นในฐานะนิติบุคคลได้อย่างไร
jhegedus

35

เรามักจะมีสอง ID ในหน่วยงานของเรา:

  1. ใช้สำหรับเลเยอร์การคงอยู่เท่านั้น (เพื่อให้ผู้ให้บริการการมีอยู่และฐานข้อมูลสามารถค้นหาความสัมพันธ์ระหว่างวัตถุได้)
  2. สำหรับความต้องการใช้งานของเรา ( equals()และhashCode()โดยเฉพาะอย่างยิ่ง)

ลองดูสิ:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

แก้ไข:เพื่อชี้แจงจุดของฉันเกี่ยวกับsetUuid()วิธีการโทร นี่เป็นสถานการณ์ทั่วไป:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

เมื่อฉันเรียกใช้การทดสอบของฉันและดูผลลัพธ์ที่บันทึกฉันแก้ไขปัญหา:

User user = new User();
user.setUuid(UUID.randomUUID());

อีกทางเลือกหนึ่งสามารถสร้างนวกรรมิกแยกต่างหาก:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

ดังนั้นตัวอย่างของฉันจะมีลักษณะเช่นนี้:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

ฉันใช้ตัวสร้างเริ่มต้นและตัวตั้งค่า แต่คุณอาจพบว่าตัวสร้างแบบสองทางมีความเหมาะสมกับคุณมากกว่า


2
ฉันเชื่อว่านี่เป็นทางออกที่ถูกต้องและดี มันอาจมีข้อได้เปรียบด้านประสิทธิภาพเพียงเล็กน้อยเนื่องจากจำนวนเต็มมักจะทำงานได้ดีกว่าในดัชนีฐานข้อมูลมากกว่า uuids แต่นอกเหนือจากนั้นคุณอาจกำจัดคุณสมบัติเลขจำนวนเต็มปัจจุบันและแทนที่ด้วย uuid (แอปพลิเคชันที่กำหนด) uuid?
Chris Lercher

4
สิ่งนี้แตกต่างจากการใช้ค่าเริ่มต้นhashCode/ equalsวิธีการสำหรับความเท่าเทียมกันของ JVM และidความคงอยู่ที่เท่าเทียมกันอย่างไร มันไม่สมเหตุสมผลสำหรับฉันเลย
Behrang Saeedzadeh

2
มันทำงานได้ในกรณีที่คุณมีวัตถุเอนทิตี้หลายแห่งชี้ไปที่แถวเดียวกันในฐานข้อมูล Object's equals()จะกลับมาfalseในกรณีนี้ UUID ตามผลตอบแทนequals() true
Andrew АндрейЛисточкин

4
-1 - ฉันไม่เห็นเหตุผลใด ๆ ที่จะมี ID สองตัวและตัวตนสองชนิด ดูเหมือนว่าไม่มีจุดหมายอย่างสมบูรณ์และอาจเป็นอันตรายต่อฉัน
Tom Anderson

1
ขออภัยที่วิจารณ์โซลูชันของคุณโดยไม่ต้องชี้ไปที่ฉันต้องการ ในระยะสั้นฉันจะให้วัตถุเขตข้อมูล ID เดียวฉันจะใช้เท่ากับและ hashCode ขึ้นอยู่กับมันและฉันจะสร้างมูลค่าในการสร้างวัตถุมากกว่าเมื่อบันทึกลงในฐานข้อมูล ด้วยวิธีนี้ทุกรูปแบบของวัตถุทำงานในลักษณะเดียวกัน: ไม่ถาวรถาวรและแยกออก พร็อกซีไฮเบอร์เนต (หรือคล้ายกัน) ควรทำงานอย่างถูกต้องและฉันคิดว่าไม่จำเป็นต้องให้ความชุ่มชื้นในการจัดการการโทรเท่ากับและ hashCode
Tom Anderson

31

โดยส่วนตัวแล้วฉันใช้สามประเภทนี้ในโครงการต่าง ๆ และฉันต้องบอกว่าตัวเลือกที่ 1 เป็นความคิดของฉันที่เป็นไปได้มากที่สุดในแอปในชีวิตจริง จากประสบการณ์ของฉันที่ทำลาย hashCode () / equals () จะนำไปสู่ข้อผิดพลาดที่บ้าคลั่งมากมายเช่นเดียวกับที่คุณจะจบลงในสถานการณ์ที่ผลของการเปลี่ยนแปลงความเท่าเทียมกันหลังจากเอนทิตีถูกเพิ่มลงในคอลเลกชัน

แต่มีตัวเลือกเพิ่มเติม (ด้วยข้อดีและข้อเสียของพวกเขา):


ก) hashCode / เท่ากับตั้งอยู่บนพื้นฐานของไม่เปลี่ยนรูป , ไม่เป็นโมฆะ , คอนสตรัคที่ได้รับมอบหมายเขตข้อมูล

(+) รับประกันสามเกณฑ์ทั้งหมด

(-) ต้องมีค่าฟิลด์เพื่อสร้างอินสแตนซ์ใหม่

(-) การจัดการมีความซับซ้อนหากคุณต้องเปลี่ยนหนึ่งในนั้น


b) hashCode / เท่ากับคีย์หลักที่กำหนดโดยแอปพลิเคชัน (ในตัวสร้าง) แทน JPA

(+) รับประกันสามเกณฑ์ทั้งหมด

(-) คุณไม่สามารถใช้ประโยชน์จากประเภทการสร้าง ID ที่เชื่อถือได้ง่ายเช่นลำดับ DB

(-) ซับซ้อนหากมีการสร้างเอนทิตีใหม่ในสภาพแวดล้อมแบบกระจาย (ไคลเอนต์ / เซิร์ฟเวอร์) หรือคลัสเตอร์เซิร์ฟเวอร์แอป


c) hashCode / เท่ากับUUID ที่กำหนดโดยตัวสร้างของเอนทิตี

(+) รับประกันสามเกณฑ์ทั้งหมด

(-) ค่าใช้จ่ายของการสร้าง UUID

(-) อาจมีความเสี่ยงเล็กน้อยที่ใช้ UUID เดียวกันสองครั้งขึ้นอยู่กับ algorythm ที่ใช้ (อาจถูกตรวจพบโดยดัชนีเฉพาะบน DB)


ฉันเป็นแฟนตัวเลือกที่ 1และApproach Cด้วย อย่าทำอะไรเลยจนกว่าคุณจะต้องการมันเป็นวิธีที่คล่องตัว
Adam Gent

2
+1 สำหรับตัวเลือก (b) IMHO หากองค์กรมี ID ธุรกิจที่เป็นธรรมชาตินั่นก็ควรเป็นคีย์หลักของฐานข้อมูล ง่ายมากตรงไปตรงมาออกแบบฐานข้อมูลที่ดี หากไม่มี ID ดังกล่าวจะต้องใช้รหัสตัวแทน หากคุณตั้งค่าไว้ที่การสร้างวัตถุทุกสิ่งทุกอย่างก็ง่าย เมื่อคนไม่ได้ใช้กุญแจธรรมชาติและไม่สร้างกุญแจตัวแทนก่อนที่พวกเขาจะมีปัญหา สำหรับความซับซ้อนในการนำไปใช้ - ใช่มีอยู่บ้าง แต่มันก็ไม่มากนักและมันก็สามารถทำได้ในแบบที่ธรรมดามาก ๆ ที่สามารถแก้มันได้ครั้งเดียวสำหรับทุกหน่วยงาน
ทอมแอนเดอร์สัน

ฉันยังต้องการตัวเลือกที่ 1 Collectionแต่แล้ววิธีการเขียนการทดสอบหน่วยที่จะยืนยันความเท่าเทียมกันเต็มรูปแบบเป็นปัญหาใหญ่เพราะเรามีการดำเนินการเท่ากับวิธีการ
ลูกบอลน้ำ OOD

อย่าทำอย่างนั้น ดูอย่ายุ่งกับไฮเบอร์เนต
ลอนนา

29

หากคุณต้องการใช้equals()/hashCode()สำหรับชุดของคุณในแง่ที่ว่าเอนทิตีเดียวกันสามารถอยู่ในนั้นได้เพียงครั้งเดียวมีตัวเลือกเดียวเท่านั้น: ตัวเลือก 2: นั่นเป็นเพราะคีย์หลักสำหรับเอนทิตีตามคำจำกัดความไม่เคยเปลี่ยนแปลง มันไม่ใช่เอนทิตีเดียวกันอีกต่อไป)

คุณควรทำสิ่งนั้นอย่างแท้จริง: เนื่องจากคุณequals()/hashCode()ใช้คีย์หลักคุณจะต้องไม่ใช้วิธีการเหล่านี้จนกว่าจะมีการตั้งค่าคีย์หลัก ดังนั้นคุณไม่ควรใส่เอนทิตีในชุดจนกว่าพวกเขาจะได้รับคีย์หลัก (ใช่ UUIDs และแนวคิดที่คล้ายกันอาจช่วยกำหนดคีย์หลักให้เร็วขึ้น)

ทีนี้ก็เป็นไปได้ในทางทฤษฎีที่จะทำเช่นนั้นด้วยตัวเลือก 3 ถึงแม้ว่าสิ่งที่เรียกว่า "คีย์ธุรกิจ" จะมีข้อเสียเปรียบที่น่ารังเกียจที่พวกเขาสามารถเปลี่ยนแปลงได้: "สิ่งที่คุณต้องทำคือลบเอนทิตีที่แทรกไว้แล้ว s) แล้วใส่กลับเข้าไปใหม่ " นั่นเป็นความจริง - แต่ก็หมายความว่าในระบบแบบกระจายคุณจะต้องตรวจสอบให้แน่ใจว่าสิ่งนี้ถูกทำขึ้นทุกที่ที่มีการแทรกข้อมูลลงไป (และคุณต้องทำให้แน่ใจว่ามีการอัปเดต ก่อนสิ่งอื่น ๆ เกิดขึ้น) คุณจะต้องมีกลไกการอัปเดตที่ซับซ้อนโดยเฉพาะหากระบบระยะไกลบางระบบไม่สามารถเข้าถึงได้ ...

ตัวเลือกที่ 1 สามารถใช้งานได้หากวัตถุทั้งหมดในชุดของคุณมาจากเซสชัน Hibernate เดียวกัน เอกสาร Hibernate ทำให้ชัดเจนในบทที่13.1.3 พิจารณาตัวตนของวัตถุ :

ภายในเซสชันแอปพลิเคชันสามารถใช้ == เพื่อเปรียบเทียบวัตถุอย่างปลอดภัย

อย่างไรก็ตามแอปพลิเคชันที่ใช้ == นอกเซสชันอาจให้ผลลัพธ์ที่ไม่คาดคิด สิ่งนี้อาจเกิดขึ้นแม้ในบางสถานที่ที่ไม่คาดคิด ตัวอย่างเช่นหากคุณใส่สองอินสแตนซ์ที่แยกออกมาไว้ในชุดเดียวกันทั้งคู่อาจมีข้อมูลประจำตัวฐานข้อมูลเหมือนกัน (กล่าวคือพวกเขาเป็นตัวแทนของแถวเดียวกัน) อย่างไรก็ตามเอกลักษณ์ JVM เป็นไปตามนิยามที่ไม่รับประกันสำหรับอินสแตนซ์ในสถานะที่แยกออก ผู้พัฒนาจะต้องแทนที่เมธอด equals () และ hashCode () ในคลาสถาวรและใช้แนวคิดเรื่องความเท่าเทียมกันของวัตถุ

มันยังคงโต้แย้งในตัวเลือก 3:

มีหนึ่ง caveat: ไม่เคยใช้ตัวระบุฐานข้อมูลเพื่อใช้ความเท่าเทียมกัน ใช้รหัสธุรกิจที่มีการรวมกันของคุณลักษณะที่ไม่ซ้ำกันมักจะไม่เปลี่ยนรูป ตัวระบุฐานข้อมูลจะเปลี่ยนแปลงหากวัตถุชั่วคราวถูกทำให้เป็นแบบถาวร หากอินสแตนซ์ชั่วคราว (โดยปกติพร้อมกับอินสแตนซ์ที่แยกออก) จะถูกเก็บไว้ในชุดการเปลี่ยนแปลง hashcode จะเป็นการแบ่งสัญญาของชุด

นี่เป็นเรื่องจริงถ้าคุณ

  • ไม่สามารถกำหนด ID ได้ล่วงหน้า (เช่นโดยใช้ UUID)
  • และยังต้องการให้วัตถุของคุณอยู่ในฉากขณะที่วัตถุอยู่ในสภาพชั่วคราว

มิฉะนั้นคุณสามารถเลือกตัวเลือก 2 ได้ฟรี

จากนั้นจะกล่าวถึงความต้องการเสถียรภาพที่สัมพันธ์กัน:

แอตทริบิวต์สำหรับคีย์ธุรกิจไม่จำเป็นต้องมีความเสถียรเท่ากับคีย์หลักของฐานข้อมูล คุณจะต้องรับประกันความมั่นคงตราบเท่าที่วัตถุอยู่ในชุดเดียวกัน

สิ่งนี้ถูกต้อง ปัญหาเชิงปฏิบัติที่ฉันเห็นในเรื่องนี้คือ: หากคุณไม่สามารถรับประกันเสถียรภาพที่แน่นอนได้คุณจะสามารถรับประกันเสถียรภาพได้อย่างไร "ตราบเท่าที่วัตถุอยู่ในชุดเดียวกัน" ฉันสามารถจินตนาการถึงกรณีพิเศษบางอย่าง (เช่นการใช้ชุดสำหรับการสนทนาและจากนั้นก็โยนทิ้งไป) แต่ฉันจะถามถึงความเหมาะสมโดยทั่วไปของสิ่งนี้


เวอร์ชั่นสั้น:

  • ตัวเลือก 1 สามารถใช้กับวัตถุภายในเซสชันเดียวเท่านั้น
  • หากคุณสามารถทำได้ให้ใช้ตัวเลือกที่ 2 (กำหนด PK ให้เร็วที่สุดเนื่องจากคุณไม่สามารถใช้วัตถุในชุดจนกว่าจะมีการกำหนด PK)
  • หากคุณสามารถรับประกันความมั่นคงสัมพัทธ์คุณสามารถใช้ตัวเลือก 3 แต่ระวังด้วยสิ่งนี้

การสันนิษฐานของคุณว่าคีย์หลักไม่เคยเปลี่ยนแปลงเป็นเท็จ เช่นไฮเบอร์เนตจัดสรรเฉพาะคีย์หลักเมื่อบันทึกเซสชัน ดังนั้นหากคุณใช้คีย์หลักเป็น hashCode ผลลัพธ์ของ hashCode () ก่อนที่คุณจะบันทึกวัตถุเป็นครั้งแรกและหลังจากที่คุณบันทึกวัตถุเป็นครั้งแรกจะแตกต่างกัน ที่แย่กว่านั้นก่อนที่คุณจะบันทึกเซสชันวัตถุที่สร้างขึ้นใหม่สองแห่งจะมี hashCode เดียวกันและสามารถเขียนทับกันเมื่อเพิ่มลงในคอลเล็กชัน คุณอาจพบว่าตัวเองต้องบังคับให้บันทึก / ล้างข้อมูลทันทีในการสร้างวัตถุเพื่อใช้แนวทาง
William Billingsley

2
@William: คีย์หลักของเอนทิตีไม่เปลี่ยนแปลง คุณสมบัติ id ของวัตถุที่แมปอาจมีการเปลี่ยนแปลง นี้เกิดขึ้นในขณะที่คุณอธิบายโดยเฉพาะอย่างยิ่งเมื่อมีการชั่วคราววัตถุที่ทำถาวร โปรดอ่านส่วนของคำตอบของฉันอย่างระมัดระวังซึ่งฉันพูดเกี่ยวกับวิธีการเท่ากับ / hashCode: "คุณต้องไม่ใช้วิธีการเหล่านี้จนกว่าจะมีการตั้งค่าคีย์หลัก"
Chris Lercher

เห็นด้วยอย่างสิ้นเชิง. ด้วยตัวเลือกที่ 2 คุณยังสามารถแยกส่วนเท่ากับ / hashcode ในระดับซูเปอร์คลาสและนำมาใช้ใหม่โดยเอนทิตีทั้งหมดของคุณ
ธีโอ

+1 ฉันเป็นมือใหม่กับ JPA แต่ความเห็นและคำตอบบางส่วนนี่หมายความว่าผู้คนไม่เข้าใจความหมายของคำว่า "คีย์หลัก"
Raedwald

16
  1. หากคุณมีความสำคัญทางธุรกิจแล้วคุณควรใช้ว่าสำหรับ/equalshashCode
  2. หากคุณไม่มีคีย์ธุรกิจคุณไม่ควรปล่อยให้มันมีค่าเริ่มต้นObjectเท่ากับและการใช้งาน hashCode เพราะมันไม่ทำงานหลังจากที่คุณmergeและนิติบุคคล
  3. คุณสามารถใช้ตัวระบุเอนทิตีตามที่แนะนำในโพสต์นี้ สิ่งที่จับได้เพียงอย่างเดียวคือคุณต้องใช้hashCodeการนำไปใช้งานซึ่งจะคืนค่าเดิมเสมอดังนี้

    @Entity
    public class Book implements Identifiable<Long> {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Book)) return false;
            Book book = (Book) o;
            return getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }

ซึ่งจะดีกว่า (1) onjava.com/pub/a/onjava/2006/09/13/...หรือ (2) vladmihalcea.com/... ? โซลูชัน (2) นั้นง่ายกว่า (1) เหตุใดฉันจึงควรใช้ (1) ผลของทั้งคู่เหมือนกันหรือไม่? ทั้งคู่รับประกันวิธีแก้ปัญหาเดียวกันหรือไม่?
nimo23

และด้วยโซลูชันของคุณ: "ค่า hashCode จะไม่เปลี่ยนแปลง" ระหว่างอินสแตนซ์เดียวกัน สิ่งนี้มีพฤติกรรมเหมือนกันกับว่าเป็น uuid "เดียวกัน" (จากโซลูชัน (1)) ที่กำลังถูกเปรียบเทียบ ฉันถูกไหม?
nimo23

1
และเก็บ UUID ในฐานข้อมูลและเพิ่ม footprint ของเรคคอร์ดและในบัฟเฟอร์พูลหรือไม่ ฉันคิดว่าสิ่งนี้สามารถนำไปสู่ปัญหาประสิทธิภาพการทำงานในระยะยาวมากกว่า hashCode ที่ไม่ซ้ำกัน สำหรับโซลูชันอื่นคุณสามารถตรวจสอบเพื่อดูว่ามีความสอดคล้องกันในการเปลี่ยนสถานะเอนทิตีทั้งหมดหรือไม่ คุณสามารถค้นหาทดสอบที่ตรวจสอบว่าในวันที่ GitHub
Vlad Mihalcea

1
หากคุณมีคีย์ธุรกิจที่ไม่เปลี่ยนรูปแบบ hashCode สามารถใช้งานได้และจะได้รับประโยชน์จากถังหลายถังดังนั้นจึงคุ้มค่าที่จะใช้หากคุณมี มิฉะนั้นเพียงใช้ตัวระบุเอนทิตีตามที่อธิบายไว้ในบทความของฉัน
Vlad Mihalcea

1
ฉันดีใจที่คุณชอบมัน. ฉันมีบทความอีกหลายร้อยเรื่องเกี่ยวกับ JPA และ Hibernate
Vlad Mihalcea

10

แม้ว่าการใช้คีย์ธุรกิจ (ตัวเลือก 3) เป็นวิธีที่แนะนำมากที่สุด ( วิกิชุมชน Hibernate , "Java Persistence with Hibernate" หน้า 398) และนี่คือสิ่งที่เราใช้เป็นส่วนใหญ่ ชุด: HHH-3799 ในกรณีนี้ไฮเบอร์เนตสามารถเพิ่มเอนทิตีลงในชุดก่อนที่ฟิลด์จะเริ่มต้นได้ ฉันไม่แน่ใจว่าทำไมข้อผิดพลาดนี้ไม่ได้รับความสนใจมากขึ้นเนื่องจากมันทำให้แนวทางธุรกิจหลักที่แนะนำมีปัญหา

ฉันคิดว่าหัวใจของเรื่องนี้ก็คือว่าควรจะเท่ากับและ hashCode อยู่บนพื้นฐานของรัฐที่ไม่เปลี่ยนแปลง (อ้างอิงOdersky et al. ) และหน่วยงานไฮเบอร์เนตที่มีคีย์หลักที่มีการจัดการ Hibernate ไม่มีสถานะที่ไม่เปลี่ยนรูปแบบ คีย์หลักถูกแก้ไขโดยไฮเบอร์เนตเมื่อวัตถุชั่วคราวกลายเป็นแบบถาวร คีย์ธุรกิจจะถูกปรับเปลี่ยนโดย Hibernate เมื่อไฮเดรตวัตถุในกระบวนการของการเริ่มต้น

ที่เหลือเพียงตัวเลือกที่ 1 สืบทอดการใช้งาน java.lang.Object ตามเอกลักษณ์ของวัตถุหรือการใช้คีย์หลักที่มีการจัดการแอปพลิเคชันที่แนะนำโดย James Brundege ใน"อย่าปล่อยให้ไฮเบอร์เนตขโมยรหัสประจำตัวของคุณ" (อ้างอิงแล้วโดยคำตอบของ Stijn Geukens ) และแลนซ์ Arlaus ใน"วัตถุ Generation: วิธีการที่ดีกว่าที่จะบูรณาการไฮเบอร์เนต"

ปัญหาที่ใหญ่ที่สุดของตัวเลือกที่ 1 คืออินสแตนซ์ที่แยกออกไม่สามารถเปรียบเทียบกับอินสแตนซ์ถาวรโดยใช้. equals () แต่ก็ไม่เป็นไร สัญญาของเท่ากับและ hashCode ปล่อยให้ผู้พัฒนาตัดสินใจว่าความเท่าเทียมกันหมายถึงอะไรสำหรับแต่ละชั้น ดังนั้นให้เท่ากับและ hashCode สืบทอดจาก Object หากคุณต้องการที่จะเปรียบเทียบเช่นออกไปตัวอย่างถาวรคุณสามารถสร้างวิธีการใหม่อย่างชัดเจนสำหรับวัตถุประสงค์ที่อาจจะboolean sameEntityหรือหรือboolean dbEquivalentboolean businessEquals


5

ฉันเห็นด้วยกับคำตอบของแอนดรู เราทำสิ่งเดียวกันในแอปพลิเคชันของเรา แต่แทนที่จะเก็บ UUIDs เป็น VARCHAR / CHAR เราแบ่งออกเป็นสองค่ายาว ดูที่ UUID.getLeastSignentialBits () และ UUID.getMostSignentiallyBits ()

สิ่งที่ต้องพิจารณาอีกอย่างคือการโทรไปที่ UUID.randomUUID () ค่อนข้างช้าดังนั้นคุณอาจต้องการดู UUID อย่างเกียจคร้านเมื่อสร้างความจำเป็นเช่นในระหว่างการคงอยู่หรือการเรียกใช้เท่ากับ () / hashCode ()

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

ที่จริงแล้วถ้าคุณแทนที่เท่ากับ () / hashCode () คุณต้องสร้าง UUID สำหรับทุกเอนทิตีอยู่ดี (ฉันคิดว่าคุณต้องการคงเอนทิตีที่คุณสร้างในโค้ดของคุณ) คุณทำได้เพียงครั้งเดียว - ก่อนที่จะเก็บไว้ในฐานข้อมูลเป็นครั้งแรก หลังจาก UUID นั้นโหลดโดย Persistence Provider ดังนั้นฉันไม่เห็นจุดที่ทำมันอย่างเกียจคร้าน
Andrew АндрейЛисточкин

ฉันโหวตให้คำตอบของคุณเพราะฉันชอบความคิดอื่น ๆ ของคุณมาก: การเก็บ UUID เป็นตัวเลขในฐานข้อมูลและไม่ได้ชี้ไปที่ประเภทเฉพาะภายในเมธอด equals () - นั่นเป็นระเบียบจริงๆ! ฉันจะใช้เทคนิคสองอย่างนี้ในอนาคต
Andrew АндрейЛисточкин

1
ขอบคุณสำหรับการโหวต เหตุผลในการเริ่มต้น UUID อย่างเกียจคร้านคือในแอปของเราเราสร้างเอนทิตีจำนวนมากที่ไม่เคยมีใน HashMap หรือยืนยันอยู่ ดังนั้นเราเห็นประสิทธิภาพลดลง 100x เมื่อเราสร้างวัตถุ (100,000 รายการ) ดังนั้นเราจึงเริ่ม UUID ก็ต่อเมื่อจำเป็น ฉันแค่หวังว่าจะมีการสนับสนุนที่ดีใน MySql สำหรับตัวเลข 128 บิตดังนั้นเราจึงสามารถใช้ UUID สำหรับ id ด้วยและไม่สนใจ auto_increment
Drew

อ้อเข้าใจแล้ว. ในกรณีของฉันเราไม่ได้ประกาศฟิลด์ UUID หากเอนทิตีที่เกี่ยวข้องจะไม่ถูกใส่ในคอลเล็กชัน ข้อเสียคือบางครั้งเราต้องเพิ่มเพราะภายหลังปรากฎว่าเราจำเป็นต้องใส่มันเข้าไปในคอลเลกชัน บางครั้งสิ่งนี้เกิดขึ้นในระหว่างการพัฒนา แต่โชคดีที่ไม่เคยเกิดขึ้นกับเราหลังจากการปรับใช้กับลูกค้าครั้งแรกดังนั้นจึงไม่ใช่เรื่องใหญ่ หากสิ่งนั้นจะเกิดขึ้นหลังจากระบบเริ่มทำงานเราจะต้องทำการย้ายฐานข้อมูล Lazy UUID มีประโยชน์มากในสถานการณ์เช่นนี้
Andrew АндрейЛисточкин

บางทีคุณควรลองใช้ตัวสร้าง UUID ที่เร็วกว่าที่ Adam แนะนำไว้ในคำตอบของเขาหากประสิทธิภาพเป็นปัญหาสำคัญในสถานการณ์ของคุณ
Andrew АндрейЛисточкин

3

ขณะที่คนอื่นฉลาดกว่าฉันได้ชี้ให้เห็นแล้วมีกลวิธีมากมายที่นั่น ดูเหมือนว่าจะเป็นไปได้ว่ารูปแบบการออกแบบที่ใช้ส่วนใหญ่พยายามที่จะแฮ็กไปสู่ความสำเร็จ พวกเขา จำกัด การเข้าถึง Constructor หากไม่ขัดขวางการเรียกใช้ Constructor อย่างสมบูรณ์ด้วย Constructor และวิธีการเฉพาะของโรงงาน แน่นอนว่ามันเป็นที่น่าพอใจเสมอด้วย API ที่ชัดเจน แต่ถ้าเหตุผลเดียวที่ทำให้การแทนที่และแฮชโค้ดนั้นเข้ากันได้กับแอพพลิเคชั่นนั้นฉันสงสัยว่ากลยุทธ์เหล่านั้นสอดคล้องกับ KISS หรือไม่ (Keep It Simple Stupid)

สำหรับฉันฉันต้องการแทนที่เท่ากับและแฮชโค้ดโดยวิธีตรวจสอบรหัส ในวิธีการเหล่านี้ฉันต้องการรหัสไม่เป็นโมฆะและบันทึกการทำงานนี้ไว้เป็นอย่างดี ดังนั้นมันจะกลายเป็นนักพัฒนาสัญญาที่จะคงอยู่นิติบุคคลใหม่ก่อนที่จะเก็บเขาไว้ที่อื่น แอปพลิเคชันที่ไม่เคารพสัญญานี้จะล้มเหลวภายในไม่กี่นาที (หวังว่า)

คำเตือนแม้ว่า: เอนทิตีของคุณจะถูกเก็บไว้ในตารางที่แตกต่างกันและผู้ให้บริการของคุณใช้กลยุทธ์การสร้างอัตโนมัติสำหรับคีย์หลักจากนั้นคุณจะได้รับคีย์หลักซ้ำในประเภทเอนทิตี ในกรณีเช่นนี้ให้เปรียบเทียบประเภทเวลาทำงานกับการเรียกไปยังObject # getClass ()ซึ่งแน่นอนว่าจะทำให้เป็นไปไม่ได้ที่สองประเภทที่แตกต่างกันจะถือว่าเท่ากัน นั่นเหมาะกับฉันดีแล้วส่วนใหญ่


แม้จะมีลำดับที่หายไปของฐานข้อมูล (เช่น Mysql) ก็เป็นไปได้ที่จะจำลองพวกเขา (เช่นตาราง hibernate_sequence) ดังนั้นคุณอาจได้รับ ID ที่ไม่ซ้ำกันในทุกตาราง +++ แต่คุณไม่ต้องการมัน การโทรObject#getClass() ไม่ดีเนื่องจากพร็อกซีเอช การโทรHibernate.getClass(o)ช่วย แต่ปัญหาความเสมอภาคของเอนทิตีที่แตกต่างกันยังคงอยู่ มีวิธีแก้ปัญหาโดยใช้canEqualค่อนข้างซับซ้อน แต่ใช้งานได้ ตกลงกันว่าโดยปกติแล้วมันไม่จำเป็น +++ การขว้าง eq / hc บน null ID เป็นการละเมิดสัญญา แต่มันก็ใช้งานได้จริง
maaartinus

2

เห็นได้ชัดว่ามีคำตอบที่ให้ข้อมูลมากแล้วที่นี่ แต่ฉันจะบอกคุณว่าเราทำอะไร

เราไม่ทำอะไรเลย (เช่นอย่าแทนที่)

ถ้าเราต้องการเท่ากับ / hashcode ในการทำงานกับคอลเลกชันที่เราใช้ UUID คุณเพียงแค่สร้าง UUID ในตัวสร้าง เราใช้http://wiki.fasterxml.com/JugHomeสำหรับ UUID UUID เป็นซีพียูที่แพงกว่านิดหน่อย แต่ราคาถูกกว่าเมื่อเทียบกับการเข้าถึงซีเรียลไลซ์เซชั่นและ db


1

ฉันเคยใช้ตัวเลือกที่ 1 ในอดีตเพราะฉันตระหนักถึงการสนทนาเหล่านี้และคิดว่าเป็นการดีกว่าที่จะไม่ทำอะไรเลยจนกว่าฉันจะรู้สิ่งที่ถูกต้อง ระบบเหล่านั้นทั้งหมดยังคงทำงานได้สำเร็จ

อย่างไรก็ตามในครั้งต่อไปฉันอาจลองใช้ตัวเลือกที่ 2 - โดยใช้รหัสฐานข้อมูลที่สร้างขึ้น

Hashcode และเท่ากับจะโยน IllegalStateException หากไม่ได้ตั้งรหัส

วิธีนี้จะป้องกันข้อผิดพลาดเล็กน้อยที่เกี่ยวข้องกับเอนทิตีที่ไม่ได้บันทึกไม่ให้ปรากฏโดยไม่คาดคิด

ผู้คนคิดอย่างไรกับแนวทางนี้


1

วิธีการใช้คีย์ธุรกิจไม่เหมาะกับเรา เราใช้ DB สร้างIDชั่วคราวชั่วคราวtempIdและแทนที่เท่ากับ () / hashcode () เพื่อแก้ปัญหาภาวะที่กลืนไม่เข้าคายไม่ออก เอนทิตีทั้งหมดเป็นลูกหลานของเอนทิตี ข้อดี:

  1. ไม่มีฟิลด์พิเศษใน DB
  2. ไม่มีการเข้ารหัสพิเศษในเอนทิตีที่สืบทอดวิธีเดียวสำหรับทั้งหมด
  3. ไม่มีปัญหาเรื่องประสิทธิภาพ (เช่นเดียวกับ UUID) การสร้าง DB Id
  4. ไม่มีปัญหากับ Hashmaps (ไม่จำเป็นต้องคำนึงถึงการใช้งานเท่ากันและอื่น ๆ )
  5. แฮชโค้ดของเอนทิตีใหม่จะไม่เปลี่ยนแปลงในเวลาแม้ว่าจะยังคงอยู่

จุดด้อย:

  1. อาจมีปัญหาเกี่ยวกับการซีเรียลไลซ์และการดีซีเรียลไลซ์
  2. แฮชโค้ดของเอนทิตีที่บันทึกไว้อาจเปลี่ยนแปลงหลังจากโหลดซ้ำจากฐานข้อมูล
  3. ไม่ยืนยันวัตถุที่ถือว่าแตกต่างกันเสมอ (อาจจะใช่มั้ย)
  4. มีอะไรอีกบ้าง?

ดูรหัสของเรา:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}

1

โปรดพิจารณาวิธีการต่อไปนี้ตามตัวระบุประเภทที่กำหนดไว้ล่วงหน้าและรหัส

สมมติฐานเฉพาะสำหรับ JPA:

  • เอนทิตีของ "ประเภท" เดียวกันและรหัสที่ไม่ใช่นัลเดียวกันนั้นถือว่าเท่ากัน
  • เอนทิตีที่ไม่ยืนยัน (สมมติว่าไม่มี ID) จะไม่เท่ากับเอนทิตีอื่น ๆ

นิติบุคคลนามธรรม:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

ตัวอย่างเอนทิตีคอนกรีต:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

ตัวอย่างการทดสอบ:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

ข้อได้เปรียบหลักที่นี่:

  • ความง่าย
  • ตรวจสอบให้แน่ใจว่าคลาสย่อยจัดเตรียมเอกลักษณ์ชนิด
  • ทำนายพฤติกรรมด้วยคลาสพร็อกซี

ข้อเสีย:

  • ต้องการให้แต่ละเอนทิตีโทร super()

หมายเหตุ:

  • ต้องการความสนใจเมื่อใช้การสืบทอด ตัวอย่างเช่นความเท่าเทียมกันของclass Aและclass B extends Aอาจขึ้นอยู่กับรายละเอียดที่เป็นรูปธรรมของแอปพลิเคชัน
  • เป็นการดีที่ใช้รหัสธุรกิจเป็นรหัส

รอคอยที่จะแสดงความคิดเห็นของคุณ


0

นี่เป็นปัญหาที่พบบ่อยในทุกระบบไอทีที่ใช้ Java และ JPA จุดปวดเกินกว่าการใช้เท่ากับ () และ hashCode () มันส่งผลกระทบต่อวิธีที่องค์กรอ้างถึงเอนทิตีและวิธีที่ลูกค้าอ้างถึงเอนทิตีเดียวกัน ฉันเห็นความเจ็บปวดมากพอที่จะไม่มีรหัสธุรกิจจนถึงจุดที่ฉันเขียนบล็อกของตัวเองเพื่อแสดงความคิดเห็นของฉัน

กล่าวโดยย่อ: ใช้ ID ลำดับสั้น ๆ ที่มนุษย์สามารถอ่านได้พร้อมรหัสนำหน้าที่มีความหมายเป็นคีย์ธุรกิจที่สร้างขึ้นโดยไม่ต้องพึ่งพาหน่วยความจำใด ๆ นอกเหนือจาก RAM เกล็ดหิมะของ Twitter เป็นตัวอย่างที่ดีมาก


0

IMO คุณมี 3 ตัวเลือกสำหรับการใช้งานเท่ากับ / hashCode

  • ใช้แอปพลิเคชั่นที่สร้างเอกลักษณ์เช่น UUID
  • ใช้งานตามคีย์ธุรกิจ
  • ดำเนินการตามคีย์หลัก

การใช้แอปพลิเคชันที่สร้างข้อมูลประจำตัวเป็นวิธีที่ง่ายที่สุด แต่มาพร้อมกับข้อเสียเล็กน้อย

  • ตัวเชื่อมช้าลงเมื่อใช้เป็น PK เนื่องจาก 128 บิตนั้นใหญ่กว่า 32 หรือ 64 บิต
  • "การดีบักยากขึ้น" เนื่องจากการตรวจสอบด้วยตาของคุณเองว่าข้อมูลบางอย่างนั้นถูกต้องแล้วค่อนข้างยาก

หากคุณสามารถทำงานกับข้อเสียเหล่านี้เพียงใช้วิธีนี้

เพื่อเอาชนะปัญหาการเข้าร่วมอย่างใดอย่างหนึ่งอาจใช้ UUID เป็นคีย์ธรรมชาติและค่าลำดับเป็นคีย์หลัก แต่จากนั้นคุณอาจยังคงประสบปัญหาการดำเนินการเท่ากับ / hashCode ในเอนทิตี child compositional ที่มีรหัสฝังตัวเนื่องจากคุณต้องการเข้าร่วม บนคีย์หลัก การใช้คีย์ธรรมชาติในรหัสเอนทิตีรองและคีย์หลักสำหรับการอ้างอิงถึงพาเรนต์เป็นการประนีประนอมที่ดี

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

IMO นี่เป็นวิธีที่สะอาดตาที่สุดเนื่องจากจะหลีกเลี่ยงข้อเสียทั้งหมดและในขณะเดียวกันก็ให้คุณค่า (UUID) ที่คุณสามารถแบ่งปันกับระบบภายนอกโดยไม่เปิดเผยภายในระบบ

ใช้งานตามคีย์ธุรกิจหากคุณสามารถคาดหวังได้ว่าจากผู้ใช้เป็นความคิดที่ดี แต่มาพร้อมกับข้อเสียเล็กน้อยเช่นกัน

เวลาส่วนใหญ่ของรหัสธุรกิจนี้จะเป็นรหัสบางประเภทที่ผู้ใช้ให้และมักจะประกอบไปด้วยคุณสมบัติหลายอย่าง

  • การเข้าร่วมช้าลงเพราะการเข้าร่วมโดยอิงจากความยาวของข้อความนั้นช้ามาก บาง DBMS อาจมีปัญหาในการสร้างดัชนีถ้าคีย์เกินความยาวที่แน่นอน
  • ในประสบการณ์ของฉันคีย์ธุรกิจมีแนวโน้มที่จะเปลี่ยนแปลงซึ่งจะต้องมีการปรับปรุงเรียงซ้อนกับวัตถุที่อ้างอิงถึง สิ่งนี้เป็นไปไม่ได้หากระบบภายนอกอ้างถึง

IMO คุณไม่ควรใช้หรือทำงานกับรหัสธุรกิจโดยเฉพาะ มันเป็นโปรแกรมเสริมที่ดีเช่นผู้ใช้สามารถค้นหาโดยใช้รหัสธุรกิจนั้นได้อย่างรวดเร็ว แต่ระบบไม่ควรใช้เพื่อการดำเนินงาน

ใช้งานตามคีย์หลักมีปัญหา แต่อาจไม่ใช่เรื่องใหญ่

หากคุณต้องการเปิดเผย id ไปยังระบบภายนอกให้ใช้วิธี UUID ที่ฉันแนะนำ ถ้าคุณทำไม่ได้คุณยังสามารถใช้วิธี UUID ได้ แต่ไม่ต้องทำ ปัญหาของการใช้ ID ที่สร้างขึ้น DBMS ในเท่ากับ / hashCode เกิดจากความจริงที่ว่าวัตถุอาจได้รับการเพิ่มลงในคอลเลกชันตามแฮก่อนที่จะกำหนด ID

วิธีที่ชัดเจนในการหลีกเลี่ยงปัญหานี้คือการไม่เพิ่มวัตถุลงในคอลเลกชันแบบแฮชก่อนกำหนด id ฉันเข้าใจว่าไม่สามารถทำได้เพราะคุณอาจต้องการขจัดข้อมูลซ้ำซ้อนก่อนกำหนดรหัสแล้ว เพื่อให้สามารถใช้งานคอลเล็กชันที่ใช้แฮชคุณยังต้องสร้างคอลเลกชันใหม่หลังจากกำหนด ID

คุณสามารถทำสิ่งนี้:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

ฉันไม่ได้ทดสอบวิธีการที่แน่นอนด้วยตัวเองดังนั้นฉันจึงไม่แน่ใจว่าการเปลี่ยนแปลงคอลเลกชันในเหตุการณ์ก่อนและหลังมีอยู่ แต่แนวคิดคือ:

  • ลบวัตถุออกจากคอลเลกชันที่ใช้แฮชชั่วคราว
  • ยืนยันมัน
  • เพิ่มวัตถุอีกครั้งไปยังคอลเลกชันตามแฮ

อีกวิธีในการแก้ปัญหานี้ก็คือการสร้างแบบจำลองแฮชที่ใช้ใหม่ทั้งหมดของคุณหลังจากการอัพเดต / คงอยู่

ในที่สุดมันก็ขึ้นอยู่กับคุณ ฉันใช้วิธีตามลำดับเป็นการส่วนตัวโดยส่วนใหญ่แล้วใช้วิธี UUID เฉพาะในกรณีที่ฉันต้องการเปิดเผยตัวบ่งชี้ไปยังระบบภายนอก


0

ด้วยรูปแบบใหม่ของinstanceofจาก java 14 คุณสามารถใช้equalsในหนึ่งบรรทัด

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

ถ้า UUID เป็นคำตอบสำหรับหลาย ๆ คนทำไมเราไม่ใช้วิธีการจากโรงงานจากเลเยอร์ธุรกิจเพื่อสร้างเอนทิตีและกำหนดคีย์หลักเมื่อถึงเวลาสร้าง

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

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

วิธีนี้เราจะได้รับคีย์หลักเริ่มต้นสำหรับเอนทิตีจากผู้ให้บริการที่มีอยู่และฟังก์ชัน hashCode () และ equals () ของเราสามารถพึ่งพาได้

นอกจากนี้เรายังสามารถประกาศตัวสร้างของรถยนต์ที่ได้รับการปกป้องและใช้การสะท้อนกลับในวิธีการทางธุรกิจของเราในการเข้าถึงพวกเขา วิธีนี้นักพัฒนาจะไม่ได้ตั้งใจที่จะยกตัวอย่างรถยนต์ด้วยใหม่ แต่ผ่านวิธีการของโรงงาน

เป็นไงบ้าง


วิธีการที่ใช้งานได้ดีถ้าคุณเต็มใจที่จะรับผลการทำงานทั้งการสร้าง guid เมื่อทำการค้นหาฐานข้อมูล
Michael Wiles

1
แล้วรถทดสอบยูนิตล่ะ? ในกรณีนี้คุณต้องเชื่อมต่อฐานข้อมูลเพื่อทำการทดสอบ? นอกจากนี้วัตถุโดเมนของคุณไม่ควรขึ้นอยู่กับการคงอยู่
jhegedus

-1

ฉันพยายามที่จะตอบคำถามนี้ด้วยตัวเองและไม่เคยมีความสุขกับการแก้ปัญหาที่พบจนกระทั่งฉันอ่านโพสต์นี้และโดยเฉพาะอย่างยิ่ง DREW ฉันชอบวิธีที่เขาขี้เกียจสร้าง UUID และเก็บไว้อย่างเหมาะสม

แต่ฉันต้องการเพิ่มความยืดหยุ่นมากขึ้นเช่นขี้เกียจสร้าง UUID เฉพาะเมื่อเข้าถึง hashCode () / equals () ก่อนการคงอยู่ของเอนทิตีครั้งแรกด้วยข้อดีของแต่ละโซลูชัน:

  • เท่ากับ () หมายถึง "วัตถุหมายถึงเอนทิตีตรรกะเดียวกัน"
  • ใช้ ID ฐานข้อมูลให้มากที่สุดเพราะเหตุใดฉันจึงต้องทำงานสองครั้ง (ข้อกังวลด้านประสิทธิภาพ)
  • ป้องกันปัญหาในขณะที่เข้าถึง hashCode () / equals () บนเอนทิตีที่ยังไม่ยืนยันและรักษาพฤติกรรมเดิมหลังจากที่ยืนยัน

ฉันขอขอบคุณข้อเสนอแนะเกี่ยวกับโซลูชันผสมของฉันด้านล่าง

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

คุณหมายถึงอะไร "UUID หนึ่งวันที่สร้างขึ้นในเอนทิตีนี้ก่อนที่ฉันจะยืนยัน" คุณช่วยยกตัวอย่างสำหรับกรณีนี้ได้ไหม
jhegedus

คุณสามารถใช้ Generationtype ที่กำหนดได้ไหม เหตุใดการสร้างตัวตนจึงจำเป็นต้องมี มันมีข้อดีกว่าที่กำหนดไว้บ้างไหม?
jhegedus

เกิดอะไรขึ้นถ้าคุณ 1) สร้าง MyEntity ใหม่ 2) ใส่ลงในรายการ 3) จากนั้นบันทึกลงในฐานข้อมูลจากนั้น 4) คุณโหลดเอนทิตีกลับจากฐานข้อมูลและ 5) ลองดูว่าอินสแตนซ์ที่โหลดอยู่ในรายการ . ฉันเดาว่ามันจะไม่เป็นแม้ว่ามันควรจะเป็น
jhegedus

ขอบคุณสำหรับความคิดเห็นแรกของคุณที่แสดงให้ฉันเห็นว่าฉันยังไม่ชัดเจนเท่าที่ควร ประการแรก "UUID หนึ่งวันที่สร้างขึ้นในเอนทิตีนี้ก่อนที่ฉันจะยืนยัน" คือการพิมพ์ผิด ... "ก่อนที่มันจะยังคงอยู่" ควรอ่านแทน สำหรับข้อสังเกตอื่น ๆ ฉันจะแก้ไขโพสต์ของฉันในไม่ช้าเพื่อพยายามอธิบายวิธีแก้ปัญหาให้ดีขึ้น
user2083808

-1

ในทางปฏิบัติดูเหมือนว่าตัวเลือกที่ 2 (คีย์หลัก) ใช้บ่อยที่สุด คีย์ธุรกิจแบบธรรมชาติและ IMMUTABLE เป็นสิ่งที่ไม่ค่อยเกิดขึ้นการสร้างและรองรับคีย์สังเคราะห์นั้นหนักเกินไปที่จะแก้ไขสถานการณ์ซึ่งอาจไม่เกิดขึ้น ดูที่การใช้งานspring-data-jpa AbstractPersistable (สิ่งเดียว: สำหรับการใช้งาน HibernateHibernate.getClass )

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

เพิ่งทราบถึงการจัดการวัตถุใหม่ใน HashSet / HashMap ในทางตรงกันข้ามตัวเลือกที่ 1 (ยังคงObjectใช้งานได้) จะใช้งานไม่ได้mergeนั่นเป็นสถานการณ์ที่พบได้บ่อยมาก

หากคุณไม่มีรหัสธุรกิจและมีความต้องการ REAL ในการจัดการเอนทิตีใหม่ในโครงสร้างแฮชให้แทนที่hashCodeค่าคงที่ตามที่แนะนำด้านล่าง Vlad Mihalcea


-2

ด้านล่างนี้เป็นโซลูชันที่ง่าย (และทดสอบ) สำหรับ Scala

  • โปรดทราบว่าการแก้ปัญหานี้ไม่สอดคล้องกับ 3 หมวดหมู่ที่ระบุในคำถาม

  • เอนทิตีทั้งหมดของฉันเป็นคลาสย่อยของ UUIDEntity ดังนั้นฉันจึงปฏิบัติตามหลักการไม่ซ้ำตัวเอง (DRY)

  • หากจำเป็นต้องใช้การสร้าง UUID สามารถแม่นยำยิ่งขึ้น (โดยใช้ตัวเลขสุ่มหลอกเพิ่มเติม)

รหัสสกาล่า:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.