Dilema kode hash () / equals () JPA

311

Ada beberapa diskusi di sini tentang entitas JPA dan hashCode()/ equals()implementasi yang harus digunakan untuk kelas entitas JPA. Sebagian besar (jika tidak semua) dari mereka bergantung pada Hibernate, tapi saya ingin membahasnya implementasi JPA-netral (saya menggunakan EclipseLink, omong-omong).

Semua implementasi yang mungkin memiliki kelebihan dan kekurangan sendiri mengenai:

  • hashCode()/ equals()Kesesuaian kontrak (kekekalan) untuk List/ Setoperasi
  • Apakah objek yang identik (misalnya dari sesi yang berbeda, proksi dinamis dari struktur data yang malas) dapat dideteksi
  • Apakah entitas berperilaku benar dalam kondisi terlepas (atau tidak bertahan)

Sejauh yang bisa saya lihat, ada tiga opsi :

  1. Jangan menimpa mereka; andalkan Object.equals()danObject.hashCode()
    • hashCode()/ equals()bekerja
    • tidak dapat mengidentifikasi objek identik, masalah dengan proksi dinamis
    • tidak ada masalah dengan entitas yang terpisah
  2. Timpa mereka, berdasarkan kunci utama
    • hashCode()Saya equals()rusak
    • identitas yang benar (untuk semua entitas yang dikelola)
    • masalah dengan entitas yang terpisah
  3. Mengganti mereka, berdasarkan pada Business-Id (bidang kunci non-primer; bagaimana dengan kunci asing?)
    • hashCode()Saya equals()rusak
    • identitas yang benar (untuk semua entitas yang dikelola)
    • tidak ada masalah dengan entitas yang terpisah

Pertanyaan saya adalah:

  1. Apakah saya melewatkan opsi dan / atau titik pro / kontra?
  2. Pilihan apa yang Anda pilih dan mengapa?



PEMBARUAN 1:

Dengan " hashCode()/ equals()rusak", maksud saya bahwa hashCode()pemanggilan yang berurutan dapat mengembalikan nilai yang berbeda, yang (bila diterapkan dengan benar) tidak rusak dalam arti Objectdokumentasi API, tetapi yang menyebabkan masalah ketika mencoba untuk mengambil entitas yang diubah dari Map,, Setatau lainnya berbasis hash Collection. Akibatnya, implementasi JPA (setidaknya EclipseLink) tidak akan berfungsi dengan benar dalam beberapa kasus.

PEMBARUAN 2:

Terima kasih atas jawaban Anda - kebanyakan dari mereka memiliki kualitas luar biasa.
Sayangnya, saya masih tidak yakin pendekatan mana yang akan menjadi yang terbaik untuk aplikasi kehidupan nyata, atau bagaimana menentukan pendekatan terbaik untuk aplikasi saya. Jadi, saya akan membuka pertanyaan dan berharap untuk beberapa diskusi dan / atau pendapat.

MRalwasser
sumber
4
Saya tidak mengerti apa yang Anda maksud dengan "hashCode () / equals () broken"
nanda
4
Mereka tidak akan "rusak" dalam pengertian itu, seperti pada opsi 2 dan 3 Anda akan mengimplementasikan kedua equals () dan hashCode () menggunakan strategi yang sama.
matt b
11
Itu tidak benar dari opsi 3. kode hash () dan equals () harus menggunakan kriteria yang sama, oleh karena itu jika salah satu bidang Anda berubah, ya metode kode hash () akan mengembalikan nilai yang berbeda untuk contoh yang sama daripada sebelumnya, tetapi akan sama dengan (). Anda telah menghapus bagian kedua kalimat dari hashcode () javadoc: Setiap kali dipanggil pada objek yang sama lebih dari sekali selama eksekusi aplikasi Java, metode hashCode harus secara konsisten mengembalikan integer yang sama, asalkan tidak ada informasi digunakan dalam perbandingan yang sama pada objek yang dimodifikasi .
matt b
1
Sebenarnya bagian kalimat itu berarti sebaliknya - memanggil hashcode()instance objek yang sama harus mengembalikan nilai yang sama, kecuali bidang apa pun yang digunakan dalam equals()perubahan implementasi. Dengan kata lain, jika Anda memiliki tiga bidang di kelas Anda dan equals()metode Anda hanya menggunakan dua bidang untuk menentukan persamaan contoh, maka Anda dapat mengharapkan nilai hashcode()pengembalian berubah jika Anda mengubah salah satu dari nilai bidang tersebut - yang masuk akal ketika Anda mempertimbangkan bahwa instance objek ini tidak lagi "sama" dengan nilai yang diwakili oleh instance lama.
matt b
2
"masalah ketika mencoba untuk mengambil entitas yang berubah dari Peta, Set atau Koleksi berbasis hash lainnya" ... ini harus menjadi "masalah ketika mencoba untuk mengambil entitas yang diubah dari HashMap, HashSet atau Koleksi berbasis hash lainnya"
nanda

Jawaban:

122

Baca artikel yang sangat bagus tentang subjek ini: Jangan Biarkan Hibernasi Mencuri Identitas Anda .

Kesimpulan artikelnya seperti ini:

Identitas objek tampak sulit untuk diimplementasikan dengan benar ketika objek-objek tetap ada dalam database. Namun, masalahnya berasal sepenuhnya dari membiarkan objek ada tanpa id sebelum disimpan. Kita bisa menyelesaikan masalah ini dengan mengambil tanggung jawab untuk menetapkan ID objek dari kerangka pemetaan objek-relasional seperti Hibernate. Sebagai gantinya, ID objek dapat diberikan segera setelah objek dipakai. Ini membuat identitas objek sederhana dan bebas kesalahan, dan mengurangi jumlah kode yang diperlukan dalam model domain.

Stijn Geukens
sumber
21
Tidak, itu bukan artikel yang bagus. Itu adalah artikel yang sangat bagus tentang masalah ini, dan harus dibaca untuk setiap programmer JPA! +1!
Tom Anderson
2
Yup saya menggunakan solusi yang sama. Tidak membiarkan DB menghasilkan ID memiliki kelebihan lain juga, seperti bisa membuat objek dan sudah membuat objek lain yang mereferensikannya sebelum bertahan. Ini dapat menghapus latensi dan beberapa siklus permintaan / respons di aplikasi server-klien. Jika Anda membutuhkan inspirasi untuk solusi semacam itu, lihat proyek saya: suid.js dan suid-server-java . Pada dasarnya suid.jsmengambil blok ID dari suid-server-javamana Anda bisa mendapatkan dan menggunakan sisi klien.
Stijn de Witt
2
Ini benar-benar gila. Saya baru mengenal cara kerja hibernasi di bawah tenda, sedang menulis unit test, dan menemukan bahwa saya tidak dapat menghapus objek dari suatu set setelah memodifikasinya, menyimpulkan bahwa itu karena perubahan kode hash, tetapi tidak dapat memahami bagaimana menyelesaikan. Artikel itu sangat sederhana!
XMight
Ini adalah artikel yang bagus. Namun, bagi orang-orang yang melihat tautan untuk pertama kalinya, saya akan menyarankan bahwa itu mungkin merupakan pembunuhan berlebihan untuk sebagian besar aplikasi. 3 opsi lain yang tercantum pada halaman ini harus lebih atau kurang menyelesaikan masalah dengan berbagai cara.
HopeKing
1
Apakah Hibernate / JPA menggunakan metode equals dan hashcode suatu entitas untuk memeriksa apakah catatan sudah ada dalam database?
Tushar Banne
64

Saya selalu menimpa sama dengan / kode hash dan menerapkannya berdasarkan id bisnis. Tampaknya solusi yang paling masuk akal bagi saya. Lihat tautan berikut .

Untuk meringkas semua hal ini, berikut adalah daftar apa yang akan berfungsi atau tidak akan bekerja dengan berbagai cara untuk menangani equals / kode hash: masukkan deskripsi gambar di sini

EDIT :

Untuk menjelaskan mengapa ini bekerja untuk saya:

  1. Saya biasanya tidak menggunakan koleksi berbasis hash (HashMap / HashSet) di aplikasi JPA saya. Jika saya harus, saya lebih suka membuat solusi UniqueList.
  2. Saya pikir mengubah id bisnis saat runtime bukan praktik terbaik untuk aplikasi database apa pun. Pada kasus yang jarang terjadi di mana tidak ada solusi lain, saya akan melakukan perawatan khusus seperti menghapus elemen dan mengembalikannya ke koleksi berbasis hash.
  3. Untuk model saya, saya menetapkan id bisnis pada konstruktor dan tidak menyediakan setter untuknya. Saya membiarkan implementasi JPA untuk mengubah bidang, bukan properti.
  4. Solusi UUID tampaknya berlebihan. Mengapa UUID jika Anda memiliki id bisnis alami? Bagaimanapun, saya akan menetapkan keunikan id bisnis dalam database. Mengapa memiliki TIGA indeks untuk setiap tabel dalam database?
nanda
sumber
1
Tetapi tabel ini tidak memiliki baris kelima "berfungsi dengan Daftar / Set" (jika Anda berpikir untuk menghapus entitas yang merupakan bagian dari Set dari pemetaan OneToMany) yang akan dijawab "Tidak" pada dua opsi terakhir karena kode hash-nya ( ) perubahan yang melanggar kontraknya.
MRalwasser
Lihat komentar pada pertanyaan. Anda tampaknya salah paham dengan kontrak equals / hashcode
nanda
1
@ McRwwerer: Saya pikir Anda maksud hal yang benar, hanya saja tidak sama dengan / kode hash () kontrak itu sendiri yang dilanggar. Tapi kode yang sama dengan / kode hash memang menciptakan masalah dengan kontrak Set .
Chris Lercher
3
@MRalwasser: Kode hash hanya dapat berubah jika ID bisnis berubah, dan intinya adalah bahwa ID bisnis tidak berubah. Jadi kode hash tidak berubah, dan ini berfungsi sempurna dengan koleksi hash.
Tom Anderson
1
Bagaimana jika Anda tidak memiliki kunci bisnis alami? Misalnya dalam kasus titik dua dimensi, Point (X, Y), dalam aplikasi menggambar grafik? Bagaimana Anda menyimpan titik itu sebagai Entitas?
jhegedus
35

Kami biasanya memiliki dua ID di entitas kami:

  1. Hanya untuk lapisan persistensi (sehingga penyedia dan database ketekunan dapat mengetahui hubungan antara objek).
  2. Apakah untuk kebutuhan aplikasi kita ( equals()dan hashCode()khususnya)

Lihatlah:

@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
}

EDIT: untuk memperjelas poin saya tentang panggilan ke setUuid()metode. Berikut ini adalah skenario yang khas:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("[email protected]");

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

Ketika saya menjalankan tes saya dan melihat output log saya memperbaiki masalahnya:

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

Atau, seseorang dapat menyediakan konstruktor yang terpisah:

@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.
}

Jadi contoh saya akan terlihat seperti ini:

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

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

Saya menggunakan konstruktor default dan setter, tetapi Anda mungkin menemukan pendekatan dua konstruktor lebih cocok untuk Anda.

Andrew Андрей Листочкин
sumber
2
Saya percaya, ini adalah solusi yang benar dan baik. Mungkin juga memiliki sedikit keunggulan kinerja, karena bilangan bulat biasanya berkinerja lebih baik dalam indeks basis data daripada uuids. Tapi selain itu, Anda mungkin bisa menghilangkan properti id integer saat ini, dan menggantinya dengan (aplikasi yang ditetapkan) uuid?
Chris Lercher
4
Bagaimana hal ini berbeda dengan menggunakan standar hashCode/ equalsmetode untuk kesetaraan JVM dan iduntuk kesetaraan kegigihan? Ini sama sekali tidak masuk akal bagi saya.
Behrang Saeedzadeh
2
Ini bekerja dalam kasus ketika Anda memiliki beberapa objek entitas yang menunjuk ke baris yang sama dalam database. Object's equals()akan kembali falsedalam kasus ini. UUID berbasis equals()kembali true.
Andrew Андрей Листочкин
4
-1 - saya tidak melihat alasan untuk memiliki dua ID, dan dua jenis identitas. Ini sepertinya sama sekali tidak berguna dan berpotensi berbahaya bagi saya.
Tom Anderson
1
Maaf telah mengkritik solusi Anda tanpa menunjuk ke yang saya inginkan. Singkatnya, saya akan memberikan objek bidang ID tunggal, saya akan menerapkan kode yang sama dan hash berdasarkan itu, dan saya akan menghasilkan nilainya pada pembuatan objek, daripada ketika menyimpan ke database. Dengan begitu, semua bentuk objek bekerja dengan cara yang sama: tidak gigih, gigih, dan terlepas. Proxy Hibernate (atau serupa) juga harus bekerja dengan benar, dan saya pikir bahkan tidak perlu terhidrasi untuk menangani panggilan kode dan hash yang sama.
Tom Anderson
31

Saya pribadi sudah menggunakan ketiga stategies ini di proyek yang berbeda. Dan saya harus mengatakan bahwa opsi 1 menurut saya adalah yang paling praktis dalam aplikasi kehidupan nyata. Dalam pengalaman saya, melanggar kode hashCode () / equals () mengarah ke banyak bug gila karena Anda akan setiap kali berakhir dalam situasi di mana hasil perubahan kesetaraan setelah entitas telah ditambahkan ke koleksi.

Tetapi ada opsi lebih lanjut (juga dengan pro dan kontra mereka):


a) kode hash / sederajat berdasarkan satu set berubah , tidak null , konstruktor ditugaskan , bidang

(+) ketiga kriteria dijamin

(-) nilai bidang harus tersedia untuk membuat instance baru

(-) mempersulit penanganan jika Anda harus mengubah salah satunya


b) kode hash / sama dengan berdasarkan kunci utama yang ditetapkan oleh aplikasi (dalam konstruktor) bukan JPA

(+) ketiga kriteria dijamin

(-) Anda tidak dapat memanfaatkan stategasi pembuatan ID sederhana yang andal seperti urutan DB

(-) rumit jika entitas baru dibuat di lingkungan terdistribusi (klien / server) atau server aplikasi


c) kode hash / sama dengan yang didasarkan pada UUID yang diberikan oleh konstruktor entitas

(+) ketiga kriteria dijamin

(-) overhead pembuatan UUID

(-) mungkin sedikit risiko bahwa dua kali UUID yang sama digunakan, tergantung pada algorythm yang digunakan (dapat dideteksi oleh indeks unik pada DB)

pemilik
sumber
Saya penggemar Opsi 1 dan Pendekatan C juga. Jangan lakukan apa pun sampai Anda benar-benar membutuhkannya, itu adalah pendekatan yang lebih gesit.
Adam Gent
2
+1 untuk opsi (b). IMHO, jika suatu entitas memiliki ID bisnis alami, maka itu juga harus menjadi kunci utama basis datanya. Itu sederhana, mudah, desain database yang bagus. Jika tidak memiliki ID seperti itu, maka kunci pengganti diperlukan. Jika Anda mengatur itu pada pembuatan objek, maka segala sesuatu yang lain adalah sederhana. Ketika orang tidak menggunakan kunci alami, dan tidak menghasilkan kunci pengganti lebih awal, mereka akan mendapat masalah. Adapun kompleksitas dalam implementasi - ya, ada beberapa. Tapi sungguh tidak banyak, dan itu bisa dilakukan dengan cara yang sangat umum yang menyelesaikannya sekali untuk semua entitas.
Tom Anderson
Saya juga lebih suka opsi 1, tetapi kemudian bagaimana menulis unit test untuk menegaskan kesetaraan penuh adalah masalah besar, karena kita harus menerapkan metode equals for Collection.
OOD Waterball
Tapi jangan lakukan itu. Lihat Don't Mess With The Hibernate
alonana
29

Jika Anda ingin menggunakan equals()/hashCode()Set Anda, dalam arti bahwa entitas yang sama hanya dapat berada di sana satu kali, maka hanya ada satu opsi: Opsi 2. Itu karena kunci utama untuk entitas menurut definisi tidak pernah berubah (jika seseorang memang memperbarui itu, itu bukan entitas yang sama lagi)

Anda harus menerimanya secara harfiah: Karena Anda equals()/hashCode()didasarkan pada kunci utama, Anda tidak boleh menggunakan metode ini, sampai kunci primer ditetapkan. Jadi Anda tidak harus meletakkan entitas di dalam himpunan, sampai mereka diberi kunci utama. (Ya, UUID dan konsep serupa dapat membantu menetapkan kunci primer lebih awal.)

Sekarang, secara teoritis juga mungkin untuk mencapai itu dengan Opsi 3, meskipun apa yang disebut "kunci bisnis" memiliki kelemahan buruk yang dapat mereka ubah: "Yang harus Anda lakukan adalah menghapus entitas yang sudah dimasukkan dari set (). s), dan masukkan kembali. " Itu benar - tetapi itu juga berarti, bahwa dalam sistem terdistribusi, Anda harus memastikan, bahwa ini dilakukan sepenuhnya di mana pun data telah dimasukkan ke (dan Anda harus memastikan, bahwa pembaruan dilakukan , sebelum hal lain terjadi). Anda akan memerlukan mekanisme pembaruan yang canggih, terutama jika beberapa sistem jarak jauh saat ini tidak dapat dijangkau ...

Opsi 1 hanya dapat digunakan, jika semua objek di set Anda berasal dari sesi Hibernate yang sama. Dokumentasi Hibernate memperjelas hal ini di bab 13.1.3. Mempertimbangkan identitas objek :

Dalam Sesi, aplikasi dapat dengan aman menggunakan == untuk membandingkan objek.

Namun, aplikasi yang menggunakan == di luar Sesi dapat menghasilkan hasil yang tidak terduga. Ini mungkin terjadi bahkan di beberapa tempat yang tidak terduga. Misalnya, jika Anda meletakkan dua instance terpisah ke dalam Set yang sama, keduanya mungkin memiliki identitas basis data yang sama (yaitu, mereka mewakili baris yang sama). Namun, identitas JVM, menurut definisi, tidak dijamin untuk kejadian dalam keadaan terpisah. Pengembang harus mengganti metode equals () dan hashCode () di kelas persisten dan menerapkan gagasan mereka sendiri tentang kesetaraan objek.

Itu terus memperdebatkan mendukung Opsi 3:

Ada satu peringatan: tidak pernah menggunakan pengidentifikasi basis data untuk menerapkan kesetaraan. Gunakan kunci bisnis yang merupakan kombinasi dari atribut unik, biasanya tidak berubah. Pengidentifikasi basis data akan berubah jika objek sementara dibuat persisten. Jika instance sementara (biasanya bersama-sama dengan instance terpisah) disimpan dalam Set, mengubah kode hash akan memutus kontrak Set.

Ini benar, jika Anda

  • tidak dapat menetapkan id lebih awal (mis. dengan menggunakan UUID)
  • namun Anda benar-benar ingin meletakkan objek Anda di set saat mereka dalam keadaan sementara.

Jika tidak, Anda bebas memilih Opsi 2.

Kemudian disebutkan perlunya stabilitas relatif:

Atribut untuk kunci bisnis tidak harus sama stabilnya dengan kunci primer basis data; Anda hanya perlu menjamin stabilitas selama objek berada di Set yang sama.

Ini benar. Masalah praktis yang saya lihat dengan ini adalah: Jika Anda tidak dapat menjamin stabilitas absolut, bagaimana Anda dapat menjamin stabilitas "selama objek berada di Set yang sama". Saya bisa membayangkan beberapa kasus khusus (seperti menggunakan set hanya untuk percakapan dan kemudian membuangnya), tetapi saya akan mempertanyakan kepraktisan umum ini.


Versi pendek:

  • Opsi 1 hanya dapat digunakan dengan objek dalam satu sesi.
  • Jika Anda bisa, gunakan Opsi 2. (Tetapkan PK sedini mungkin, karena Anda tidak dapat menggunakan objek di set sampai PK ditugaskan.)
  • Jika Anda dapat menjamin stabilitas relatif, Anda dapat menggunakan Opsi 3. Tapi hati-hati dengan ini.
Chris Lercher
sumber
Asumsi Anda bahwa kunci utama tidak pernah berubah adalah salah. Misalnya, Hibernate hanya mengalokasikan kunci utama saat sesi disimpan. Jadi, jika Anda menggunakan kunci utama sebagai kode hash Anda, hasil dari kode hash () sebelum Anda pertama kali menyimpan objek dan setelah Anda pertama kali menyimpan objek akan berbeda. Lebih buruk lagi, sebelum Anda menyimpan sesi, dua objek yang baru dibuat akan memiliki kode hash yang sama dan dapat saling menimpa ketika ditambahkan ke koleksi. Anda mungkin harus memaksa save / flush segera pada pembuatan objek untuk menggunakan pendekatan itu.
William Billingsley
2
@ William: Kunci utama suatu entitas tidak berubah. Properti id dari objek yang dipetakan dapat berubah. Ini terjadi, seperti yang Anda jelaskan, terutama ketika objek sementara dibuat gigih . Harap baca bagian dari jawaban saya dengan hati-hati, di mana saya mengatakan tentang metode equals / hashCode: "Anda tidak boleh menggunakan metode ini, sampai kunci utama ditetapkan."
Chris Lercher
Setuju. Dengan opsi 2, Anda juga dapat memfaktorkan equals / hashcode di kelas super dan menggunakannya kembali oleh semua entitas Anda.
Theo
+1 Saya baru mengenal JPA, tetapi beberapa komentar dan jawaban di sini menyiratkan bahwa orang tidak memahami arti istilah "kunci utama".
Raedwald
16
  1. Jika Anda memiliki kunci bisnis , maka Anda harus menggunakannya untuk equals/ hashCode.
  2. Jika Anda tidak memiliki kunci bisnis, Anda tidak boleh membiarkannya Objectsama dengan implementasi default dan kode hash karena itu tidak berfungsi setelah Anda mergedan entitas.
  3. Anda dapat menggunakan pengenal entitas seperti yang disarankan dalam posting ini . Satu-satunya tangkapan adalah bahwa Anda perlu menggunakan hashCodeimplementasi yang selalu mengembalikan nilai yang sama, seperti ini:

    @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
    }
Vlad Mihalcea
sumber
Mana yang lebih baik: (1) onjava.com/pub/a/onjava/2006/09/13/… atau (2) vladmihalcea.com/… ? Solusi (2) lebih mudah daripada (1). Jadi mengapa saya harus menggunakan (1). Apakah efek keduanya sama? Apakah keduanya menjamin solusi yang sama?
nimo23
Dan dengan solusi Anda: "nilai kode hash tidak berubah" antara instance yang sama. Ini memiliki perilaku yang sama seolah-olah itu adalah uuid "sama" (dari solusi (1)) dibandingkan. Apakah saya benar?
nimo23
1
Dan menyimpan UUID di database dan meningkatkan jejak catatan dan di buffer pool? Saya pikir ini dapat menyebabkan lebih banyak masalah kinerja dalam jangka panjang daripada kode hash yang unik. Adapun solusi lain, Anda dapat memeriksanya untuk melihat apakah itu memberikan konsistensi di semua transisi status entitas. Anda dapat menemukan tes yang memeriksa itu di GitHub .
Vlad Mihalcea
1
Jika Anda memiliki kunci bisnis yang tidak dapat diubah, kode hash dapat menggunakannya dan itu akan mendapat manfaat dari banyak bucket, jadi layak digunakan jika Anda memilikinya. Kalau tidak, gunakan saja pengenal entitas seperti yang dijelaskan dalam artikel saya.
Vlad Mihalcea
1
Saya senang Anda menyukainya. Saya punya ratusan artikel lagi tentang JPA dan Hibernate.
Vlad Mihalcea
10

Meskipun menggunakan kunci bisnis (opsi 3) adalah pendekatan yang paling sering direkomendasikan ( wiki komunitas Hibernate , "Java Persistence with Hibernate" hal. 398), dan inilah yang paling sering kami gunakan, ada bug Hibernate yang memecah ini untuk diambil dengan cepat. set: HHH-3799 . Dalam hal ini, Hibernate dapat menambahkan entitas ke set sebelum bidangnya diinisialisasi. Saya tidak yakin mengapa bug ini tidak mendapat perhatian lebih, karena itu benar-benar membuat pendekatan kunci bisnis yang direkomendasikan menjadi masalah.

Saya pikir inti permasalahannya adalah bahwa equals dan kode hash harus didasarkan pada status tidak berubah (referensi Odersky et al. ), Dan entitas Hibernate dengan kunci utama yang dikelola Hibernate tidak memiliki status tetap seperti itu. Kunci utama dimodifikasi oleh Hibernate ketika objek transien menjadi persisten. Kunci bisnis juga dimodifikasi oleh Hibernate, ketika menghidrasi sebuah objek dalam proses diinisialisasi.

Yang tersisa hanya opsi 1, mewarisi implementasi java.lang.Object berdasarkan identitas objek, atau menggunakan kunci primer yang dikelola aplikasi seperti yang disarankan oleh James Brundege di "Jangan Biarkan Hibernasi Curi Identitas Anda" (sudah dirujuk oleh jawaban Stijn Geukens ) dan oleh Lance Arlaus dalam " Pembuatan Objek: Pendekatan yang Lebih Baik untuk Hibernate Integration" .

Masalah terbesar dengan opsi 1 adalah bahwa instance terpisah tidak dapat dibandingkan dengan instance persisten menggunakan .equals (). Tapi tidak apa-apa; kontrak equals dan hashCode menyerahkan kepada pengembang untuk memutuskan apa arti kesetaraan untuk setiap kelas. Jadi biarkan equals dan hashCode mewarisi dari Object. Jika Anda perlu membandingkan turunan terpisah dengan turunan persisten, Anda dapat membuat metode baru secara eksplisit untuk tujuan itu, mungkin boolean sameEntityatau boolean dbEquivalentatau boolean businessEquals.

jbyler
sumber
5

Saya setuju dengan jawaban Andrew. Kami melakukan hal yang sama dalam aplikasi kami tetapi alih-alih menyimpan UUID sebagai VARCHAR / CHAR, kami membaginya menjadi dua nilai panjang. Lihat UUID.getLeastSignificantBits () dan UUID.getMostSignificantBits ().

Satu hal lagi yang perlu dipertimbangkan, adalah bahwa panggilan ke UUID.randomUUID () cukup lambat, jadi Anda mungkin ingin melihat pembuatan UUID secara malas hanya saat dibutuhkan, seperti saat ketekunan atau panggilan untuk equals () / 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();
    }

}
Drew
sumber
Ya, sebenarnya jika Anda mengganti equals () / hashCode () maka Anda harus menghasilkan UUID untuk setiap entitas (saya berasumsi bahwa Anda ingin tetap menggunakan setiap entitas yang Anda buat dalam kode Anda). Anda melakukannya hanya sekali - sebelum menyimpannya ke database untuk pertama kalinya. Setelah itu UUID baru dimuat oleh Persistence Provider. Jadi saya tidak melihat titik melakukannya dengan malas.
Andrew Андрей Листочкин
Saya memilih ulang jawaban Anda karena saya sangat menyukai ide-ide Anda yang lain: menyimpan UUID sebagai pasangan angka dalam basis data dan tidak melakukan casting ke tipe tertentu di dalam metode equals () - yang benar-benar rapi! Saya pasti akan menggunakan dua trik ini di masa depan.
Andrew Андрей Листочкин
1
Terima kasih atas suaranya. Alasan malas menginisialisasi UUID ada di aplikasi kami, kami membuat banyak Entitas yang tidak pernah dimasukkan ke dalam HashMap atau bertahan. Jadi kami melihat 100x penurunan kinerja ketika kami membuat objek (100.000 di antaranya). Jadi kita hanya init UUID jika diperlukan. Saya hanya berharap ada dukungan yang baik di MySql untuk angka 128 bit sehingga kita bisa menggunakan UUID untuk id juga dan tidak peduli dengan auto_increment.
Drew
Oh begitu. Dalam kasus saya, kami bahkan tidak menyatakan bidang UUID jika entitas yang sesuai tidak akan dimasukkan ke dalam koleksi. Kekurangannya adalah bahwa kadang-kadang kita harus menambahkannya karena nanti ternyata kita benar-benar perlu meletakkannya di koleksi. Ini kadang-kadang terjadi selama pengembangan tetapi untungnya tidak pernah terjadi pada kami setelah penempatan awal ke pelanggan sehingga itu bukan masalah besar. Jika itu akan terjadi setelah sistem ditayangkan kami akan memerlukan migrasi db. UUID yang malas sangat membantu dalam situasi seperti itu.
Andrew Андрей Листочкин
Mungkin Anda juga harus mencoba generator UUID yang lebih cepat yang disarankan Adam dalam jawabannya jika kinerja merupakan masalah kritis dalam situasi Anda.
Andrew Андрей Листочкин
3

Seperti yang sudah ditunjukkan oleh orang lain yang lebih pintar dari saya, ada banyak strategi di luar sana. Namun sepertinya sebagian besar pola desain yang diterapkan mencoba meretas jalan mereka menuju kesuksesan. Mereka membatasi akses konstruktor jika tidak menghalangi permintaan konstruktor sepenuhnya dengan konstruktor khusus dan metode pabrik. Memang selalu menyenangkan dengan API yang jelas. Tetapi jika satu-satunya alasan adalah untuk membuat equals- dan kode hash menimpa kompatibel dengan aplikasi, maka saya bertanya-tanya apakah strategi tersebut sesuai dengan KISS (Keep It Simple Stupid).

Bagi saya, saya suka menimpa sama dan kode hash dengan cara memeriksa id. Dalam metode ini, saya meminta id untuk tidak menjadi nol dan mendokumentasikan perilaku ini dengan baik. Dengan demikian akan menjadi kontrak pengembang untuk bertahan entitas baru sebelum menyimpannya di tempat lain. Aplikasi yang tidak menghormati kontrak ini akan gagal dalam hitungan menit (semoga).

Namun, peringatan: Jika entitas Anda disimpan di tabel yang berbeda dan penyedia Anda menggunakan strategi pembuatan otomatis untuk kunci utama, maka Anda akan mendapatkan duplikat kunci utama di seluruh tipe entitas. Dalam kasus seperti itu, juga membandingkan jenis waktu berjalan dengan panggilan ke Object # getClass () yang tentu saja akan membuat tidak mungkin bahwa dua jenis yang berbeda dianggap sama. Itu cocok untuk saya sebagian besar.

Martin Andersson
sumber
Bahkan dengan urutan yang hilang DB (seperti Mysql), dimungkinkan untuk mensimulasikan mereka (misalnya, tabel hibernate_ berikutnyaence). Jadi, Anda selalu dapat memperoleh ID unik di seluruh tabel. +++ Tetapi Anda tidak membutuhkannya. Memanggil Object#getClass() itu buruk karena H. proxy. Memanggil Hibernate.getClass(o)membantu, tetapi masalah kesetaraan entitas dari berbagai jenis tetap. Ada solusi menggunakan canEqual , sedikit rumit, tetapi dapat digunakan. Setuju bahwa biasanya itu tidak diperlukan. +++ Melempar dalam persamaan / hc pada null ID melanggar kontrak, tapi itu sangat pragmatis.
maaartinus
2

Jelas sudah ada jawaban yang sangat informatif di sini, tetapi saya akan memberi tahu Anda apa yang kami lakukan.

Kami tidak melakukan apa pun (yaitu jangan menimpa).

Jika kita membutuhkan equals / hashcode untuk bekerja untuk koleksi, kita menggunakan UUID. Anda cukup membuat UUID di konstruktor. Kami menggunakan http://wiki.fasterxml.com/JugHome untuk UUID. UUID sedikit lebih mahal daripada CPU tetapi lebih murah dibandingkan dengan serialisasi dan akses db.

Adam Gent
sumber
1

Saya selalu menggunakan opsi 1 di masa lalu karena saya menyadari diskusi ini dan berpikir lebih baik tidak melakukan apa-apa sampai saya tahu hal yang benar untuk dilakukan. Semua sistem itu masih berjalan dengan sukses.

Namun, lain kali saya dapat mencoba opsi 2 - menggunakan database yang dihasilkan Id.

Hashcode dan equals akan melempar IllegalStateException jika id tidak disetel.

Ini akan mencegah kesalahan halus yang melibatkan entitas yang belum disimpan muncul secara tidak terduga.

Apa pendapat orang tentang pendekatan ini?

Neil Stevens
sumber
1

Pendekatan kunci bisnis tidak cocok untuk kami. Kami menggunakan ID yang dihasilkan DB , tempId sementara sementara dan mengesampingkan equal () / hashcode () untuk menyelesaikan dilema. Semua entitas adalah keturunan Entitas. Pro:

  1. Tidak ada bidang tambahan dalam DB
  2. Tidak ada pengkodean tambahan dalam entitas keturunan, satu pendekatan untuk semua
  3. Tidak ada masalah kinerja (seperti dengan UUID), pembuatan DB Id
  4. Tidak ada masalah dengan Hashmaps (tidak perlu mengingat penggunaan sama & dll.)
  5. Hashcode entitas baru tidak berubah dalam waktu bahkan setelah bertahan

Cons:

  1. Mungkin ada masalah dengan serialisasi dan deserializing entitas tidak bertahan
  2. Hashcode entitas yang disimpan dapat berubah setelah memuat ulang dari DB
  3. Objek yang tidak bertahan dianggap selalu berbeda (mungkin ini benar?)
  4. Apa lagi?

Lihatlah kode kami:

@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();
    }
}
Demel
sumber
1

Silakan pertimbangkan pendekatan berikut berdasarkan pengidentifikasi tipe yang telah ditentukan dan ID.

Asumsi khusus untuk JPA:

  • entitas dengan "tipe" yang sama dan ID bukan-nol yang sama dianggap sama
  • entitas non-persisten (dengan asumsi tidak ada ID) tidak pernah sama dengan entitas lain

Entitas abstrak:

@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;
  }
}

Contoh entitas konkret:

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

Contoh uji:

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

Keuntungan utama di sini:

  • kesederhanaan
  • memastikan subclass memberikan identitas tipe
  • perilaku yang diprediksi dengan kelas proksi

Kekurangan:

  • Memerlukan setiap entitas untuk menelepon super()

Catatan:

  • Perlu perhatian saat menggunakan warisan. Misalnya, kesetaraan class Adan class B extends Adapat bergantung pada detail konkret dari aplikasi.
  • Idealnya, gunakan kunci bisnis sebagai ID

Menantikan komentar Anda.

aux
sumber
0

Ini adalah masalah umum di setiap sistem TI yang menggunakan Java dan JPA. Titik nyeri melampaui penerapan equals () dan hashCode (), itu memengaruhi bagaimana suatu organisasi merujuk pada suatu entitas dan bagaimana kliennya merujuk pada entitas yang sama. Saya telah melihat cukup banyak rasa sakit karena tidak memiliki kunci bisnis sampai-sampai saya menulis blog sendiri untuk mengekspresikan pandangan saya.

Singkatnya: gunakan ID sekuensial pendek yang dapat dibaca manusia dengan awalan yang berarti sebagai kunci bisnis yang dihasilkan tanpa ketergantungan pada penyimpanan apa pun selain RAM. Snowflake Twitter adalah contoh yang sangat bagus.

Christopher Yang
sumber
0

IMO Anda memiliki 3 opsi untuk mengimplementasikan equals / hashCode

  • Gunakan identitas aplikasi yang dihasilkan yaitu UUID
  • Terapkan berdasarkan kunci bisnis
  • Terapkan berdasarkan kunci utama

Menggunakan identitas yang dihasilkan aplikasi adalah pendekatan termudah, tetapi dilengkapi dengan beberapa kelemahan

  • Bergabung lebih lambat saat menggunakannya sebagai PK karena 128 Bit hanya lebih besar dari 32 atau 64 Bit
  • "Debugging lebih sulit" karena memeriksa dengan mata kepala sendiri apakah beberapa data benar cukup sulit

Jika Anda dapat mengatasi kelemahan ini , gunakan saja pendekatan ini.

Untuk mengatasi masalah join, seseorang dapat menggunakan UUID sebagai kunci alami dan nilai urutan sebagai kunci primer, tetapi kemudian Anda mungkin masih mengalami masalah implementasi equals / hashCode di entitas anak komposisi yang memiliki id yang disematkan karena Anda ingin bergabung berdasarkan pada kunci utama. Menggunakan kunci alami dalam id entitas anak dan kunci utama untuk merujuk ke induk adalah kompromi yang baik.

@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 ini adalah pendekatan terbersih karena akan menghindari semua kerugian dan pada saat yang sama memberikan Anda nilai (UUID) yang dapat Anda bagikan dengan sistem eksternal tanpa mengekspos sistem internal.

Menerapkannya berdasarkan kunci bisnis jika Anda dapat berharap bahwa dari pengguna adalah ide yang bagus, tetapi juga memiliki beberapa kelemahan.

Sebagian besar waktu kunci bisnis ini akan menjadi semacam kode yang disediakan pengguna dan jarang gabungan dari beberapa atribut.

  • Bergabung lebih lambat karena bergabung berdasarkan teks panjang variabel hanya lambat. Beberapa DBMS bahkan mungkin memiliki masalah dalam membuat indeks jika kunci melebihi panjang tertentu.
  • Dalam pengalaman saya, kunci bisnis cenderung berubah yang akan membutuhkan pembaruan berjenjang ke objek yang merujuknya. Ini tidak mungkin jika sistem eksternal merujuknya

IMO Anda tidak boleh menerapkan atau bekerja dengan kunci bisnis secara eksklusif. Ini adalah add-on yang bagus yaitu pengguna dapat dengan cepat mencari dengan kunci bisnis itu, tetapi sistem tidak seharusnya bergantung padanya untuk beroperasi.

Menerapkannya berdasarkan kunci utama memiliki masalah, tapi mungkin itu bukan masalah besar

Jika Anda perlu mengekspos id ke sistem eksternal, gunakan pendekatan UUID yang saya sarankan. Jika tidak, Anda masih bisa menggunakan pendekatan UUID tetapi Anda tidak harus melakukannya. Masalah menggunakan id yang dihasilkan oleh DBMS sama dengan / kode hash berasal dari kenyataan bahwa objek mungkin telah ditambahkan ke koleksi berbasis hash sebelum menetapkan id.

Cara yang jelas untuk menyiasatinya adalah dengan tidak menambahkan objek ke koleksi berbasis hash sebelum menetapkan id. Saya mengerti bahwa ini tidak selalu mungkin karena Anda mungkin ingin deduplikasi sebelum menetapkan id. Untuk tetap dapat menggunakan koleksi berbasis hash, Anda hanya perlu membangun kembali koleksi setelah menetapkan id.

Anda dapat melakukan sesuatu seperti ini:

@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
}

Saya sendiri belum menguji pendekatan yang tepat, jadi saya tidak yakin bagaimana mengubah koleksi di acara pra dan pasca-persisten bekerja tetapi idenya adalah:

  • Hapus sementara objek dari koleksi berbasis hash
  • Bertahanlah
  • Tambahkan kembali objek ke koleksi berbasis hash

Cara lain untuk menyelesaikan ini adalah dengan hanya membangun kembali semua model berbasis hash Anda setelah pembaruan / bertahan.

Pada akhirnya, terserah Anda. Saya pribadi menggunakan pendekatan berbasis urutan sebagian besar waktu dan hanya menggunakan pendekatan UUID jika saya perlu mengekspos pengidentifikasi ke sistem eksternal.

Christian Beikov
sumber
0

Dengan gaya baru instanceofdari java 14, Anda dapat menerapkannya equalsdalam satu baris.

@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;
}
Artem
sumber
-1

Jika UUID adalah jawaban bagi banyak orang, mengapa kita tidak hanya menggunakan metode pabrik dari lapisan bisnis untuk membuat entitas dan menetapkan kunci utama pada waktu pembuatan?

sebagai contoh:

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

dengan cara ini kita akan mendapatkan kunci primer default untuk entitas dari penyedia persistence, dan fungsi kode hash () dan equals () kita bisa mengandalkan itu.

Kami juga dapat menyatakan bahwa konstruktor Mobil dilindungi dan kemudian menggunakan refleksi dalam metode bisnis kami untuk mengaksesnya. Dengan cara ini pengembang tidak akan berniat membuat Instantiate dengan yang baru, tetapi melalui metode pabrik.

Bagaimana dengan itu?

illEatYourPuppies
sumber
Sebuah pendekatan yang bekerja sangat baik jika Anda ingin mengambil kinerja baik menghasilkan panduan ketika melakukan pencarian basis data.
Michael Wiles
1
Bagaimana dengan unit testing Car? Dalam hal ini Anda memerlukan koneksi database untuk pengujian? Selain itu, objek domain Anda seharusnya tidak bergantung pada ketekunan.
jhegedus
-1

Saya mencoba menjawab pertanyaan ini sendiri dan tidak pernah benar-benar senang dengan solusi yang ditemukan sampai saya membaca posting ini dan terutama DREW satu. Saya suka cara dia malas membuat UUID dan menyimpannya secara optimal.

Tapi saya ingin menambahkan lebih banyak fleksibilitas, yaitu malas membuat UUID SAJA ketika hashCode () / equals () diakses sebelum persistensi pertama entitas dengan keunggulan masing-masing solusi:

  • equals () berarti "objek mengacu pada entitas logis yang sama"
  • gunakan ID database sebanyak mungkin karena mengapa saya harus melakukan pekerjaan dua kali (masalah kinerja)
  • mencegah masalah saat mengakses kode hash () / equals () pada entitas yang belum ada dan menjaga perilaku yang sama setelah itu tetap ada

Saya akan sangat menghargai umpan balik pada solusi campuran saya di bawah ini

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;
    }
pengguna2083808
sumber
apa yang Anda maksud dengan "UUID suatu hari dihasilkan di entitas ini sebelum saya bertahan"? dapatkah Anda memberi contoh untuk kasus ini?
jhegedus
bisakah Anda menggunakan jenis generasi yang ditugaskan? mengapa tipe generasi identitas perlu? apakah ada kelebihan dibandingkan dengan yang ditugaskan?
jhegedus
apa yang terjadi jika Anda 1) membuat MyEntity baru, 2) memasukkannya ke dalam daftar, 3) kemudian menyimpannya ke database kemudian 4) Anda memuat entitas itu kembali dari DB dan 5) mencoba untuk melihat apakah instance yang dimuat ada dalam daftar . Dugaan saya adalah bahwa hal itu tidak akan terjadi meskipun memang seharusnya demikian.
jhegedus
Terima kasih atas komentar pertama Anda yang menunjukkan bahwa saya tidak sejelas yang seharusnya. Pertama, "UUID suatu hari dihasilkan pada entitas ini sebelum saya bertahan" adalah kesalahan ketik ... "sebelum TI tetap ada" seharusnya dibaca sebagai gantinya. Untuk komentar lain, saya akan segera mengedit posting saya untuk mencoba menjelaskan solusi saya dengan lebih baik.
user2083808
-1

Dalam praktiknya tampaknya, Opsi 2 (kunci Primer) paling sering digunakan. Kunci bisnis yang alami dan tidak dapat digerakkan jarang terjadi, membuat dan mendukung kunci sintetis terlalu berat untuk menyelesaikan situasi, yang mungkin tidak pernah terjadi. Lihatlah implementasi AbstractPersistable spring-data-jpa (satu-satunya: untuk penggunaan implementasi 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;
}

Hanya sadar memanipulasi objek baru di HashSet / HashMap. Sebaliknya, Opsi 1 (tetap Objectimplementasi) rusak setelah itu merge, itu adalah situasi yang sangat umum.

Jika Anda tidak memiliki kunci bisnis dan memiliki kebutuhan NYATA untuk memanipulasi entitas baru dalam struktur hash, hashCodeganti ke konstan, seperti di bawah ini Vlad Mihalcea disarankan.

Grigory Kislin
sumber
-2

Di bawah ini adalah solusi sederhana (dan teruji) untuk Scala.

  • Perhatikan bahwa solusi ini tidak sesuai dengan salah satu dari 3 kategori yang diberikan dalam pertanyaan.

  • Semua Entitas saya adalah subkelas dari UUIDEntity jadi saya mengikuti prinsip don't-repeat-yourself (DRY).

  • Jika diperlukan, pembuatan UUID dapat dibuat lebih tepat (dengan menggunakan lebih banyak angka pseudo-acak).

Kode Scala:

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()
}
jhegedus
sumber