JPA hashCode () / eşittir () ikilemi


311

Olmuştur bazı tartışmalar JPA varlıkları ve yaklaşık burada hashCode()/ equals()JPA varlık sınıfları için kullanılmalıdır uygulanması. Bunların çoğu (hepsi değilse de) Hazırda Bekleme'ye bağlıdır, ancak bunları JPA-uygulama-tarafsız olarak tartışmak istiyorum (bu arada EclipseLink kullanıyorum).

Tüm olası uygulamaların kendi avantaj ve dezavantajları vardır :

  • hashCode()/equals() Sözleşme uygunluk için (değişmezlik) List/ Setoperasyonlar
  • İster aynı nesneler (örneğin, farklı oturumlar lazily yüklenmiş veri yapılarının dinamik proxy) tespit edilebilir
  • Varlıkların bağımsız (veya kalıcı olmayan) durumda doğru davranıp davranmadığı

Görebildiğim kadarıyla üç seçenek var :

  1. Onları geçersiz kılmayın; güven Object.equals()veObject.hashCode()
    • hashCode()/ equals()
    • özdeş nesneleri, dinamik proxy'lerle ilgili sorunları tanımlayamıyor
    • müstakil varlıklarla sorun yok
  2. Birincil anahtara göre bunları geçersiz kılın
    • hashCode()/ equals()bozuk
    • doğru kimlik (tüm yönetilen varlıklar için)
    • müstakil varlıklarla ilgili sorunlar
  3. Business-ID'ye (birincil olmayan anahtar alanları; yabancı anahtarlar ne olacak?) Dayalı olarak bunları geçersiz kılın.
    • hashCode()/ equals()bozuk
    • doğru kimlik (tüm yönetilen varlıklar için)
    • müstakil varlıklarla sorun yok

Sorularım:

  1. Bir seçeneği ve / veya pro / con noktasını kaçırdım mı?
  2. Hangi seçeneği seçtiniz ve neden?



GÜNCELLEME 1:

"By hashCode()/ equals()kırılır", bunu izleyen demek hashCode()(doğru bir şekilde uygulandığında zaman) anlamında kırık olmadığı, farklı değerleri döndürebilir çağırmaları ObjectAPI belgeleri, ama bir de değişmiş varlık almaya çalışırken hangi sorunlara neden olmaktadır Map, Setya da diğer karma tabanlı Collection. Sonuç olarak, JPA uygulamaları (en azından EclipseLink) bazı durumlarda düzgün çalışmayacaktır.

GÜNCELLEME 2:

Yanıtlarınız için teşekkür ederiz - çoğu dikkate değer bir kaliteye sahiptir.
Ne yazık ki, gerçek bir uygulama için hangi yaklaşımın en iyi olacağından veya başvurum için en iyi yaklaşımın nasıl belirleneceğinden hala emin değilim. Bu yüzden, soruyu açık tutacağım ve daha fazla tartışma ve / veya görüş umuyorum.


4
"HashCode () / equals () broken" ile ne demek istediğini anlamıyorum
nanda

4
Bu anlamda "kırılmış" olmazlar, seçenek 2 ve 3'te olduğu gibi, aynı stratejiyi kullanarak hem equals () hem de hashCode () yöntemlerini uygularsınız.
matt b

11
Bu seçenek 3 için geçerli değildir. HashCode () ve equals () aynı ölçütleri kullanmalıdır, bu nedenle alanlarınızdan biri değişirse, evet hashcode () yöntemi aynı örnek için öncekinden farklı bir değer döndürür, ama eşittir (). Sen karma kodudur () javadoc gelen cümlenin ikinci kısmı kapalı bıraktım: Bir Java uygulamasının bir yürütme sırasında birden çok kez aynı nesne üzerinde çağrıldığında, tutarlı bir şekilde aynı tamsayı dönmelidir hashCode yöntemi, hiçbir bilgi verdi kullanılan nesne üzerinde eşit karşılaştırmalar değiştirilir .
matt b

1
Aslında cümlenin bu kısmı hashcode(), equals()uygulamada kullanılan herhangi bir alan değişmedikçe , aynı nesne örneğini çağırmanın tersi aynı değeri döndürmesi gerektiği anlamına gelir . Başka bir deyişle, sınıfınızda üç alan varsa ve equals()yönteminiz örneklerin eşitliğini belirlemek için bunlardan yalnızca ikisini kullanıyorsa hashcode(), bu alanın değerlerinden birini değiştirirseniz dönüş değerinin değişmesini bekleyebilirsiniz. bu nesne örneğinin artık eski örneğin temsil ettiği değere "eşit" olmadığı anlamına gelir.
matt b

2
"değiştirilmiş bir varlığı Harita, Küme veya diğer karma tabanlı Koleksiyonlardan almaya çalışırken" "bu" "bir HashMap, HashSet veya diğer karma tabanlı Koleksiyonlardan değiştirilen bir varlığı almaya çalışırken karşılaşılan sorunlar" olmalıdır
nanda

Yanıtlar:


122

Konuyla ilgili bu çok güzel makaleyi okuyun: Hazırda Bekletme Kimliğinizi Çalmasına İzin Verme .

Makalenin sonucu şöyle:

Nesneler bir veritabanında kalmaya devam ettiğinde, nesne kimliğinin doğru bir şekilde uygulanması zordur. Ancak, sorunlar tamamen nesnelerin kaydedilmeden önce kimlik olmadan var olmasına izin vermekten kaynaklanır. Bu sorunları, nesne kimlikleri Hazırda Bekleme gibi nesne ilişkisel eşleme çerçevelerinden ayırma sorumluluğunu alarak çözebiliriz. Bunun yerine, nesne başlatıldığında nesne kimlikleri atanabilir. Bu, nesne kimliğini basit ve hatasız hale getirir ve etki alanı modelinde gereken kod miktarını azaltır.


21
Hayır, bu hoş bir makale değil. Bu konu hakkında harika bir makale ve her JPA programcısı için okunması gerekiyor! 1!
Tom Anderson

2
Evet, aynı çözümü kullanıyorum. DB'nin kimliğini oluşturmasına izin vermemek, bir nesne oluşturabilmek ve buna devam etmeden önce ona başvuran diğer nesneleri oluşturmak gibi başka avantajlara da sahiptir. Bu, istemci-sunucu uygulamalarındaki gecikmeyi ve birden çok istek / yanıt döngüsünü kaldırabilir. Böyle bir çözüm için ilham almanız gerekiyorsa, projelerime göz atın : suid.js ve suid-server-java . Temel olarak , daha sonra istemci tarafı alabileceğiniz ve kullanabileceğiniz suid.jskimlik blokları getirir suid-server-java.
Stijn de Witt

2
Bu sadece delilik. Çalışmaları kaputun altında hazırda bekletmek için yeniyim, birim testleri yazıyordum ve bir nesneyi değiştirdikten sonra silemediğimi öğrendim, bunun hashcode değişikliği nedeniyle olduğu sonucuna vardı, ancak nasıl olduğunu anlayamadık çözmek için. Makale basit muhteşem!
XMight

Harika bir makale. Ancak, bağlantıyı ilk kez gören insanlar için, çoğu uygulama için aşırıya kaçma olabileceğini öneririm. Bu sayfada listelenen diğer 3 seçenek, sorunu az çok çeşitli şekillerde çözmelidir.
HopeKing

1
Hazırda Bekletme / JPA, kaydın veritabanında zaten var olup olmadığını denetlemek için bir varlığın eşittir ve hashcode yöntemini kullanıyor mu?
Tushar Banne

64

Her zaman eşittir / karma kodunu geçersiz kılar ve iş kimliğine göre uygularım. Benim için en makul çözüm gibi görünüyor. Aşağıdaki bağlantıya bakın .

Tüm bunları özetlemek için, eşittir / hashCode'u işlemek için farklı yollarla neyin işe yarayıp neyin işe yaramayacağını gösteren bir liste: resim açıklamasını buraya girin

DÜZENLE :

Bunun benim için neden işe yaradığını açıklamak için:

  1. JPA uygulamamda genellikle karma tabanlı koleksiyonu (HashMap / HashSet) kullanmıyorum. Gerekirse, UniqueList çözümü oluşturmayı tercih ederim.
  2. Çalışma zamanında iş kimliğini değiştirmek herhangi bir veritabanı uygulaması için en iyi uygulama olmadığını düşünüyorum. Başka bir çözümün bulunmadığı nadir durumlarda, elemanı çıkarmak ve hashed tabanlı koleksiyona geri koymak gibi özel tedavi yaparım.
  3. Modelim için, iş kimliğini yapıcıya ayarladım ve bunun için ayarlayıcı sağlamıyorum. JPA uygulamasının özellik yerine alanı değiştirmesine izin verdim .
  4. UUID çözümü aşırıya kaçmış gibi görünüyor. Doğal işletme kimliğiniz varsa neden UUID? Sonuçta veritabanında iş kimliğinin benzersizliğini ayarlar. Neden veritabanındaki her tablo için ÜÇ dizin var?

1
Ancak bu tablo beşinci satırdan yoksun "Liste / Kümeler ile çalışır" (bir Setin bir parçası olan OneToMany eşlemesinden kaldırmayı düşünüyorsanız) son iki seçenekte "Hayır" olarak yanıtlanacak hashCode ( ) sözleşmesini ihlal eden değişiklikler.
MRalwasser

Soru hakkındaki açıklamaya bakın. Eşittir / hashcode sözleşmesini yanlış anlamışsınız gibi görünüyor
nanda

1
@MRalwasser: Sanırım doğru olanı kastediyorsun, sadece ihlal edilen equals / hashCode () sözleşmesinin kendisi değil. Ancak değişebilir bir / hashCode değeri Set sözleşmesinde sorun yaratır .
Chris Lercher

3
@MRalwasser: karma kodudur sadece iş kimliği değişirse değiştirebilir ve nokta iş kimliği olmasıdır gelmez değiştirin. Bu nedenle, karma kodu değişmez ve bu, karma koleksiyonlarla mükemmel çalışır.
Tom Anderson

1
Doğal bir işletme anahtarınız yoksa ne olur? Örneğin, iki boyutlu bir nokta olması durumunda, bir grafik çizim uygulamasında Nokta (X, Y)? Bu noktayı bir Varlık olarak nasıl saklarsınız?
jhegedus

35

Varlıklarımızda genellikle iki kimliğimiz vardır:

  1. Sadece kalıcılık katmanı içindir (böylece kalıcılık sağlayıcısı ve veritabanı nesneler arasındaki ilişkileri anlayabilir).
  2. Uygulama ihtiyaçlarımız için ( equals()ve hashCode()özellikle)

Bir göz at:

@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:setUuid() yöntem çağrıları ile ilgili benim noktaya açıklık getirmek . İşte tipik bir senaryo:

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

Testlerimi çalıştırıp günlük çıktısını gördüğümde sorunu çözdüm:

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

Alternatif olarak, ayrı bir yapıcı sağlanabilir:

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

Yani benim örneğim şöyle görünecekti:

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

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

Varsayılan bir kurucu ve bir ayarlayıcı kullanıyorum, ancak iki kurucu yaklaşımını sizin için daha uygun bulabilirsiniz.


2
Bunun doğru ve iyi bir çözüm olduğuna inanıyorum. Tamsayılar da küçük bir performans avantajına sahip olabilir, çünkü tamsayılar veri tabanı dizinlerinde genellikle uuids'ten daha iyi performans gösterir. Ancak bunun dışında, geçerli tamsayı kimliği özelliğini ortadan kaldırabilir ve (uygulama atanmış) uuid ile değiştirebilir misiniz?
Chris Lercher

4
Bu , JVM eşitliği ve kalıcılık eşitliği için varsayılan hashCode/ equalsyöntemleri kullanmaktan ne kadar farklıdır id? Bu benim için hiç mantıklı değil.
Behrang Saeedzadeh

2
Bir veritabanında aynı satıra işaret eden birkaç varlık nesneniz olduğunda çalışır. Object'ın equals()dönecekti falsebu durumda. UUID tabanlı equals()döndürür true.
Andrew Андрей Листочкин

4
-1 - İki kimliğe ve iki tür kimliğe sahip olmak için herhangi bir neden göremiyorum. Bu tamamen anlamsız ve potansiyel olarak zararlı görünüyor.
Tom Anderson

1
Ben tercih ediyorum birini işaret etmeden çözüm eleştirmek için üzgünüm. Kısacası, ben nesneleri tek bir kimlik alanı verirdi, ben eşit ve hashCode dayalı uygulamak ve ben veritabanına kaydederken yerine nesne oluşturma değeri üretecekti. Bu şekilde, nesnenin tüm formları aynı şekilde çalışır: kalıcı olmayan, kalıcı ve bağımsız. Hazırda bekleme proxy (veya benzeri) de düzgün çalışması gerekir ve ben bile eşittir ve hashCode çağrıları işlemek için hidrate gerekmez düşünüyorum.
Tom Anderson

31

Şahsen bu üç durumun hepsini farklı projelerde kullandım. Ve seçenek 1'in gerçek hayat uygulamasında en pratik olduğunu söylemek gerekir. Deneyimlerime göre, hashCode () / equals () uyumluluğunu kırmak, bir koleksiyona bir varlık eklendikten sonra eşitlik sonucunun değiştiği durumlarda her zaman sona ereceği gibi birçok çılgın hataya yol açar.

Ancak başka seçenekler de var (artıları ve eksileri ile):


a) hashCode / kümesi dayalı eşittir değişmez , değil boş , atanan yapıcı , alanlar

(+) her üç kriter de garanti altına alınmıştır

(-) alan değerleri yeni bir örnek oluşturmak için kullanılabilir olmalıdır

(-) eğer birini değiştirmek zorunda kalırsanız kullanımı zorlaştırır


b) hashCode /, uygulama tarafından JPA yerine (yapıcıda) atanan birincil anahtara eşittir

(+) her üç kriter de garanti altına alınmıştır

(-) DB dizileri gibi basit güvenilir kimlik oluşturma durumlarından yararlanamazsınız

(-) dağıtılmış bir ortamda (istemci / sunucu) veya uygulama sunucusu kümesinde yeni varlıklar oluşturulursa karmaşık


c) hashCode / varlığın kurucusu tarafından atanan bir UUID'ye eşittir

(+) her üç kriter de garanti altına alınmıştır

(-) UUID üretim yükü

(-) kullanılan algoritmaya bağlı olarak aynı UUID'nin iki katının kullanılması riski olabilir (DB'deki benzersiz bir dizin tarafından algılanabilir)


Ben de Seçenek 1 ve Yaklaşım C hayranıyım . Kesinlikle ihtiyacınız olana kadar hiçbir şey yapmayın, daha çevik bir yaklaşımdır.
Adam Gent

2
Seçenek (b) için +1. IMHO, bir işletmenin doğal bir işletme kimliğine sahip olması durumunda, bu aynı zamanda veritabanı birincil anahtarı olmalıdır. Bu basit, anlaşılır, iyi bir veritabanı tasarımıdır. Böyle bir kimliği yoksa, bir yedek anahtar gerekir. Bunu nesne oluşturmada ayarlarsanız, diğer her şey basittir. İnsanların doğal bir anahtar kullanmadığı ve sorun yaşamaları için bir yedek anahtar üretmediği zamandır. Uygulamadaki karmaşıklığa gelince - evet, bazıları var. Ama gerçekten çok değil ve tüm varlıklar için bir kez çözen çok genel bir yol yapılabilir.
Tom Anderson

Ben de 1. seçeneği tercih ederim, ama sonra tam eşitliği iddia etmek için birim testi nasıl yazılır büyük bir problemdir, çünkü eşittir yöntemini uygulamak zorundayız Collection.
OOD Waterball

Sadece yapma. Bkz Do hazırda not Mess With
alonana

29

equals()/hashCode()Kümeleriniz için kullanmak istiyorsanız , aynı varlığın sadece bir kez orada olabilmesi açısından, tek bir seçenek vardır: Seçenek 2. Bunun nedeni , bir varlığın birincil anahtarının tanımı gereği asla değişmemesidir (eğer biri gerçekten güncellenirse) artık aynı varlık değil)

Bunu tam anlamıyla almalısınız: equals()/hashCode()Birincil anahtarı temel aldığınızdan, birincil anahtar ayarlanana kadar bu yöntemleri kullanmamalısınız. Dolayısıyla, birincil anahtar atanana kadar varlıkları kümeye koymamalısınız. (Evet, UUID'ler ve benzer kavramlar birincil anahtarları erkenden atamanıza yardımcı olabilir.)

Şimdi, "iş anahtarları" olarak adlandırılabilecek kötü bir dezavantaja sahip olsa da, Seçenek 3 ile bunu başarmak teorik olarak da mümkündür: "Tek yapmanız gereken önceden eklenen varlıkları setten silmek ( ekleyin) ve yeniden takın. " Bu doğrudur - ama aynı zamanda, dağıtılmış bir sistemde, verilerin kesinlikle eklendiği her yerde yapıldığından emin olmanız gerektiği anlamına gelir (ve güncellemenin gerçekleştirildiğinden emin olmanız gerekir) , başka şeyler gerçekleşmeden önce). Özellikle bazı uzak sistemlere şu anda ulaşılamıyorsa, gelişmiş bir güncelleme mekanizmasına ihtiyacınız olacaktır ...

Seçenek 1 yalnızca, kümelerinizdeki tüm nesneler aynı Hazırda Bekletme oturumundaysa kullanılabilir. Hazırda Bekletme belgeleri, 13.1.3 bölümünde bunu açıkça ortaya koymaktadır . Nesne kimliği göz önüne alındığında :

Bir Oturum içinde uygulama, nesneleri karşılaştırmak için güvenli bir şekilde == kullanabilir.

Ancak, Oturum dışında == kullanan bir uygulama beklenmedik sonuçlar doğurabilir. Bu, bazı beklenmedik yerlerde bile ortaya çıkabilir. Örneğin, aynı Kümeye iki bağımsız örnek koyarsanız, her ikisi de aynı veritabanı kimliğine sahip olabilir (yani, aynı satırı temsil eder). Bununla birlikte, JVM kimliği, tanımı gereği, bağımsız bir durumdaki örnekler için garanti edilmez. Geliştirici, kalıcı sınıflarda equals () ve hashCode () yöntemlerini geçersiz kılmalı ve kendi nesne eşitliği kavramını uygulamalıdır.

3. Seçenek lehine tartışmaya devam etmektedir:

Bir uyarı var: Eşitliği uygulamak için asla veritabanı tanımlayıcısını kullanmayın. Benzersiz, genellikle değişmez özelliklerin birleşiminden oluşan bir işletme anahtarı kullanın. Geçici bir nesne kalıcı hale getirilirse veritabanı tanımlayıcısı değişecektir. Geçici örnek (genellikle ayrı örneklerle birlikte) bir Kümede tutulursa, karma kodun değiştirilmesi Kümenin sözleşmesini bozar.

Bu doğrudur, eğer sizin

  • kimliği erken atayamazsınız (örneğin UUID'leri kullanarak)
  • ve yine de nesnelerini geçici durumdayken setlere koymak istersiniz.

Aksi takdirde, Seçenek 2'yi seçmekte özgürsünüz.

Sonra göreceli bir istikrar ihtiyacından bahsediyor:

İş anahtarları için özniteliklerin veritabanı birincil anahtarları kadar kararlı olması gerekmez; yalnızca nesneler aynı Sette olduğu sürece istikrarı garanti etmeniz gerekir.

Doğru. Bununla ilgili gördüğüm pratik sorun: Eğer mutlak istikrarı garanti edemezseniz, "nesneler aynı Sette olduğu sürece" istikrarı nasıl garanti edersiniz? Bazı özel durumları hayal edebiliyorum (setleri sadece bir konuşma için kullanmak ve sonra atmak gibi), ancak bunun genel uygulanabilirliğini sorgulayacağım.


Kısa versiyon:

  • Seçenek 1 yalnızca tek bir oturumdaki nesnelerle kullanılabilir.
  • Mümkünse Seçenek 2'yi kullanın. (PK atanana kadar kümelerdeki nesneleri kullanamayacağınız için mümkün olduğunca erken PK atayın.)
  • Göreceli kararlılığı garanti edebiliyorsanız Seçenek 3'ü kullanabilirsiniz. Ancak buna dikkat edin.

Birincil anahtarın asla değişmediği varsayımınız yanlıştır. Örneğin, Hazırda Bekletme yalnızca oturum kaydedildiğinde birincil anahtarı ayırır. Bu nedenle, birincil anahtarı hashCode'unuz olarak kullanırsanız, nesneyi ilk kaydetmeden önce ve ilk kaydettikten sonra hashCode () sonucu farklı olacaktır. Daha da kötüsü, oturumu kaydetmeden önce yeni oluşturulan iki nesne aynı hashCode'a sahip olur ve koleksiyonlara eklendiğinde birbirlerinin üzerine yazabilir. Kendinizi, bu yaklaşımı kullanmak için nesne oluşturmaya hemen bir kaydetme / yıkama zorlamak zorunda bulabilirsiniz.
William Billingsley

2
@William: Bir varlığın birincil anahtarı değişmez. Eşlenen nesnenin id özelliği değişebilir. Bu, açıkladığınız gibi, özellikle geçici bir nesne kalıcı hale getirildiğinde gerçekleşir . Lütfen equals / hashCode yöntemleri hakkında söylediğim cevabımın bölümünü dikkatle okuyun: "birincil anahtar ayarlanana kadar bu yöntemleri kullanmamalısınız."
Chris Lercher

Tamamen katılıyorum. Seçenek 2 ile, bir süper sınıfta eşittir / hashcode'u hesaplayabilir ve tüm varlıklarınız tarafından yeniden kullanılmasını sağlayabilirsiniz.
Theo

+1 JPA için yeniyim, ancak buradaki yorumların ve cevapların bazıları, insanların "birincil anahtar" teriminin anlamını anlamadıklarını ima ediyor.
Raedwald

16
  1. Bir iş anahtarınız varsa , equals/ için bunu kullanmalısınız hashCode.
  2. Bir iş anahtarınız yoksa, Objectsiz mergeve varlıktan sonra çalışmadığı için varsayılan eşitler ve hashCode uygulamalarıyla bırakmamalısınız .
  3. Sen olabilir bu yazı önerildiği üzere varlık tanımlayıcı kullanmak . Tek yakalama, hashCodeher zaman aynı değeri döndüren bir uygulama kullanmanız gerektiğidir , örneğin:

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

Hangisi daha iyi: (1) onjava.com/pub/a/onjava/2006/09/13/… veya (2) vladmihalcea.com/… ? Çözelti (2), (1) 'den daha kolaydır. Öyleyse neden kullanmalıyım (1). İkisinin etkileri aynı mı? Her ikisi de aynı çözümü garanti ediyor mu?
nimo23

Ve sizin çözümünüzle: aynı örnekler arasında "hashCode değeri değişmez". Bu, karşılaştırılan "aynı" uuid (çözelti (1)) ile aynı davranışa sahiptir. Haklı mıyım?
nimo23

1
Ve UUID'yi veritabanında saklayın ve kaydın ve arabellek havuzunun kapladığı alanı artırın. Bunun uzun vadede benzersiz hashCode'dan daha fazla performans sorununa yol açabileceğini düşünüyorum. Diğer çözüme gelince, tüm varlık durumu geçişlerinde tutarlılık sağlayıp sağlamadığını kontrol edebilirsiniz. Bunu kontrol eden testi GitHub'da bulabilirsiniz .
Vlad Mihalcea

1
Değişmez bir iş anahtarınız varsa, hashCode bunu kullanabilir ve birden fazla kovadan faydalanacaktır, bu yüzden eğer bir tane varsa kullanmaya değer. Aksi takdirde, makalemde açıklanan varlık tanımlayıcısını kullanmanız yeterlidir.
Vlad Mihalcea

1
Beğendiğine sevindim. Ben yüzlercesi JPA ve Hibernate hakkında makaleler.
Vlad Mihalcea

10

Her ne kadar bir iş anahtarı kullanmak (seçenek 3) en çok önerilen yaklaşım olsa da ( Hazırda Bekleme topluluk wiki'si , "Hazırda Bekletme ile Java Kalıcılığı" s. 398) ve bu çoğunlukla en çok kullandığımız şeydir, bunu hevesli bir şekilde ihlal eden bir Hazırda Bekletme hatası vardır. kümeleri: HHH-3799 . Bu durumda, Hazırda Beklet, alanları başlatılmadan önce bir kümeye varlık ekleyebilir. Bu hatanın neden daha fazla dikkat çekmediğinden emin değilim, çünkü önerilen iş anahtarı yaklaşımını gerçekten sorunlu hale getiriyor.

Meselenin kalbi, eşittir ve hashCode'un değişmez duruma (referans Odersky ve ark. ) Dayandırılması ve Hazırda Bekletme tarafından yönetilen birincil anahtarı olan bir Hazırda Bekletme varlığının böyle bir değişmez durumu olmamasıdır . Geçici bir nesne kalıcı hale geldiğinde birincil anahtar Hazırda Bekletme tarafından değiştirilir. İş anahtarı, başlatma işlemindeki bir nesneyi nemlendirdiğinde Hazırda Bekletme tarafından da değiştirilir.

Bu, yalnızca nesne 1'i bırakır, java.lang.Object uygulamalarını nesne kimliğine dayalı olarak veya James Brundege tarafından "Hazırda Bekletme Kimliğinizi Çalmasına İzin Verme " (zaten Stijn Geukens'in cevabına referansta bulunma) ) ve "Nesne Üretimi: Hazırda Bekletme Entegrasyonuna Daha İyi Bir Yaklaşım" da Lance Arlaus tarafından yapılmıştır .

Seçenek 1 ile ilgili en büyük sorun, ayrılmış örneklerin .equals () kullanan kalıcı örneklerle karşılaştırılamamasıdır. Ama sorun değil; eşittir ve hashCode sözleşmesi, her sınıf için eşitliğin ne anlama geldiğine karar vermek için geliştiriciye bırakır. Sadece equals ve hashCode nesnelerini devralsın. Ayrılmış bir örneği kalıcı bir örnekle karşılaştırmanız gerekiyorsa, bu amaç için açık bir şekilde yeni bir yöntem boolean sameEntityveya belki de boolean dbEquivalentveya boolean businessEquals.


5

Andrew'un cevabına katılıyorum. Uygulamamızda da aynı şeyi yapıyoruz, ancak UUID'leri VARCHAR / CHAR olarak depolamak yerine, onu iki uzun değere ayırıyoruz. Bkz. UUID.getLeastSignificantBits () ve UUID.getMostSignificantBits ().

Dikkate alınması gereken bir şey daha, UUID.randomUUID () çağrılarının oldukça yavaş olmasıdır, bu nedenle UUID'yi yalnızca gerektiğinde tembel olarak oluşturmak veya kalıcılık veya equals () / hashCode () çağrıları gibi bakmak isteyebilirsiniz.

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

}

Aslında, equals () / hashCode () geçersiz kılıyorsanız, yine de her varlık için UUID oluşturmak zorunda (Kodunuzda oluşturduğunuz her varlık devam etmek istediğinizi varsayalım). İlk kez bir veritabanına kaydetmeden önce yalnızca bir kez yaparsınız. Bundan sonra UUID Azim Sağlayıcı tarafından yüklenir. Bu yüzden tembelce yapmanın üçüncü noktasını görmüyorum.
Andrew Андрей Листочкин

Cevabınızı oyladım çünkü diğer fikirlerinizi gerçekten seviyorum: UUID'yi veritabanında bir çift sayı olarak saklamak ve equals () yöntemi içinde belirli bir türe döküm yapmamak - gerçekten temiz! Gelecekte bu iki numarayı kesinlikle kullanacağım.
Andrew Андрей Листочкин

1
Oylama için teşekkürler. UUID'yi tembel olarak başlatmanın nedeni, uygulamamızda hiçbir zaman bir HashMap'e koyulmayan veya kalıcı olmayan birçok Varlık oluşturduk. Böylece, nesneyi oluştururken performansta 100 kat düşüş gördük (100.000 tanesi). Bu yüzden UUID'yi sadece gerekliyse başlatırız. Keşke sadece 128 bit sayıları için MySql'de iyi bir destek olsaydı, bu yüzden sadece UUID'yi id için de kullanabilirdik ve auto_increment'i umursamayız.
Drew

Ah anlıyorum. Benim durumumda, ilgili varlık koleksiyonlara konulmayacaksa UUID alanını bile ilan etmiyoruz. Dezavantajı bazen eklememiz gerektiğinden, daha sonra onları koleksiyonlara koymamız gerektiği ortaya çıkıyor. Bu bazen geliştirme sırasında olur, ancak neyse ki bir müşteriye ilk dağıtımdan sonra bize hiç olmadı, bu yüzden büyük bir anlaşma değildi. Sistem yayına girdikten sonra bu gerçekleşirse, bir db geçişine ihtiyacımız olurdu. Tembel UUID bu gibi durumlarda çok yardımcı olur.
Andrew Андрей Листочкин

Belki de performans durumunuzda kritik bir sorunsa Adam'ın cevabında önerdiği daha hızlı UUID jeneratörünü denemelisiniz.
Andrew Андрей Листочкин

3

Benden daha akıllıca işaret ettiği diğer insanlar gibi, orada çok sayıda strateji var. Yine de, uygulanan tasarım modellerinin çoğunun başarıya giden yolu kesmeye çalıştığı görülmektedir. Uzman inşaatçılar ve fabrika yöntemleri ile kurucu çağrılarını tamamen engellemiyorlarsa, kurucu erişimini sınırlarlar. Gerçekten de, açık bir API ile her zaman hoştur. Ancak tek neden, eşit ve karma kod geçersiz kılmalarını uygulama ile uyumlu hale getirmekse, bu stratejilerin KISS (Keep It Simple Aptal) ile uyumlu olup olmadığını merak ediyorum.

Benim için, kimliği inceleyerek eşitleri ve hashcode'u geçersiz kılmak istiyorum. Bu yöntemlerde, kimliğin boş olmamasını ve bu davranışı iyi belgelemesini istiyorum. Böylece onu başka bir yerde saklamadan önce yeni bir varlığın devam etmesi geliştiricilerin sözleşmesi haline gelecektir. Bu sözleşmeyi yerine getirmeyen bir başvuru bir dakika içinde başarısız olacaktır (umarım).

Dikkatli olun: Varlıklarınız farklı tablolarda depolanıyorsa ve sağlayıcınız birincil anahtar için bir otomatik oluşturma stratejisi kullanıyorsa, varlık türleri arasında çoğaltılmış birincil anahtarlar alırsınız. Böyle bir durumda, çalışma zamanı türlerini Object # getClass () çağrısıyla da elbette ki iki farklı türün eşit kabul edilmesini imkansız kılacaktır. Bu bana çok yakışıyor.


Bir DB eksik dizilerle bile (Mysql gibi), onları simüle etmek mümkündür (örn. Tablo hibernate_sequence). Böylece, tablolar arasında her zaman benzersiz bir kimlik alabilirsiniz. +++ Ama buna ihtiyacınız yok. Object#getClass() H. vekillerinden dolayı çağrı kötü. Arama Hibernate.getClass(o)yardımcı olur, ancak farklı türdeki varlıkların eşitliği sorunu devam eder. CanEqual kullanan , biraz karmaşık ama kullanışlı bir çözüm var . Genellikle gerekli olmadığını kabul etti. +++ Boş kimliğe eq / hc şeklinde atmak sözleşmeyi ihlal eder, ancak çok pragmatiktir.
maaartinus

2

Burada zaten çok bilgilendirici cevaplar var ama size ne yaptığımızı anlatacağım.

Hiçbir şey yapmıyoruz (geçersiz kılma).

Koleksiyonlarda çalışmak için eşittir / karma koduna ihtiyacımız olursa, UUID kullanırız. Sadece yapıcıda UUID oluşturun. UUID için http://wiki.fasterxml.com/JugHome kullanıyoruz . UUID biraz daha pahalı bir CPU zekasıdır, ancak serileştirme ve db erişimine kıyasla ucuzdur.


1

Geçmişte her zaman 1. seçeneği kullandım çünkü bu tartışmaların farkındaydım ve yapılacak doğru şeyi bilinceye kadar hiçbir şey yapmamanın daha iyi olacağını düşündüm. Bu sistemlerin hepsi hala başarılı bir şekilde çalışıyor.

Ancak, bir dahaki sefere ben seçenek 2 deneyebilirsiniz - oluşturulan veritabanı kimliği kullanarak.

Kimlik ayarlanmamışsa Hashcode ve equals, IllegalStateException özel durumunu atar.

Bu, kaydedilmemiş varlıkları içeren küçük hataların beklenmedik şekilde görünmesini engelleyecektir.

İnsanlar bu yaklaşım hakkında ne düşünüyor?


1

İş anahtarları yaklaşımı bizim için uygun değil. İkilemi çözmek için DB tarafından oluşturulan kimlik , geçici geçici tempId kullanıyoruz ve eşit () / hashcode () geçersiz kılınıyoruz . Tüm varlıklar Varlığın torunlarıdır. Artıları:

  1. DB'de fazladan alan yok
  2. Torun varlıklarında fazladan kodlama yok, herkes için bir yaklaşım
  3. Performans sorunu yok (UUID gibi), DB Kimliği oluşturma
  4. Hashmaps ile ilgili bir sorun yok (eşit vb. Kullanımını aklınızda bulundurmanıza gerek yok)
  5. Yeni varlığın karma kodu, devam ettikten sonra bile zaman içinde değişmedi

Eksileri:

  1. Kalıcı olmayan varlıkların serileştirilmesi ve serileştirilmesi ile ilgili sorunlar olabilir
  2. Kaydedilen varlığın karma kodu DB'den yeniden yüklendikten sonra değişebilir
  3. Kalıcı olmayan nesneler her zaman farklı kabul edilir (belki de bu doğru mu?)
  4. Başka?

Kodumuza bakın:

@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

Lütfen önceden tanımlanmış tür tanımlayıcısına ve kimliğe dayalı olarak aşağıdaki yaklaşımı göz önünde bulundurun.

JPA için özel varsayımlar:

  • aynı "tip" ve aynı null olmayan kimliğe sahip varlıklar eşit kabul edilir
  • ısrarcı olmayan varlıklar (kimlik olmadığı varsayılarak) asla diğer varlıklara eşit değildir

Soyut varlık:

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

Somut varlık örneği:

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

Test örneği:

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

Buradaki ana avantajlar:

  • basitlik
  • alt sınıfların tip kimliği sağlamasını sağlar
  • yakın sınıflarla öngörülen davranış

Dezavantajları:

  • Her varlığın aranmasını gerektirir super()

Notlar:

  • Kalıtım kullanılırken dikkat edilmesi gerekir. Örneğin , uygulamanın eşitliği class Ave class B extends Asomut ayrıntılarına bağlı olabilir.
  • İdeal olarak, kimlik olarak bir işletme anahtarı kullanın

Yorumlarınızı dört gözle bekliyoruz.


0

Bu, Java ve JPA kullanan her BT sisteminde yaygın bir sorundur. Acı noktası eşittir () ve hashCode () uygulamalarının ötesine uzanır, bir kuruluşun bir kuruma nasıl başvurduğunu ve müşterilerinin aynı kuruma nasıl başvurduğunu etkiler. Görüşümü ifade etmek için kendi blogumu yazdığım noktaya bir iş anahtarına sahip olmamak için yeterince acı gördüm .

Kısacası: RAM dışındaki herhangi bir depolama birimine bağımlı olmadan oluşturulan iş anahtarı olarak anlamlı önekleri olan kısa, insan tarafından okunabilen sıralı bir kimlik kullanın. Twitter'ın Kar Tanesi buna çok iyi bir örnek.


0

IMO eşittir / hashCode uygulamak için 3 seçeneğiniz var

  • Uygulama tarafından oluşturulan bir kimlik kullanın, yani bir UUID
  • Bir iş anahtarına dayalı olarak uygulayın
  • Birincil anahtara göre uygulayın

Uygulama tarafından oluşturulan bir kimlik kullanmak en kolay yaklaşımdır, ancak birkaç dezavantajı vardır

  • PK olarak kullanıldığında birleştirmeler daha yavaştır, çünkü 128 Bit 32 veya 64 Bit'ten daha büyüktür
  • "Hata ayıklama daha zor" çünkü bazı verilerin doğru olup olmadığını kendi gözlerinizle kontrol etmek oldukça zor

Bu dezavantajlarla çalışabiliyorsanız , sadece bu yaklaşımı kullanın.

Birleştirme sorununun üstesinden gelmek için, UUID doğal anahtar ve sıra değeri birincil anahtar olarak kullanılıyor olabilir, ancak yine de, temelde katılmak isteyeceğinizden, kimlikleri katıştırılmış bileşimli alt varlıklarda eşit / hashCode uygulama sorunlarıyla karşılaşabilirsiniz. birincil anahtarda. Alt öğe kimliğinde doğal anahtarı ve üst öğeye başvurmak için birincil anahtarı kullanmak iyi bir uzlaşmadır.

@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, tüm olumsuzluklardan kaçınacağı ve aynı zamanda size, sistem içlerini göstermeden harici sistemlerle paylaşabileceğiniz bir değer (UUID) sunacağı için en temiz yaklaşımdır.

Bir kullanıcıdan hoş bir fikir olmasını bekliyorsanız, ancak birkaç dezavantajla birlikte gelirse, bir iş anahtarına dayalı olarak uygulayın.

Çoğu zaman bu iş anahtarı , kullanıcının sağladığı bir tür kod ve daha az sıklıkla çoklu özelliklerin bir bileşimi olacaktır.

  • Değişken uzunluktaki metne dayalı birleştirme basitçe yavaş olduğundan birleşimler daha yavaştır. Bazı DBMS, anahtar belirli bir uzunluğu aşarsa dizin oluşturmada sorun yaşayabilir.
  • Deneyimlerime göre, iş anahtarları, ona atıfta bulunan nesnelere basamaklı güncelleştirmeler gerektiren değiştirme eğilimindedir. Harici sistemler buna atıfta bulunursa bu mümkün değildir

IMO, yalnızca bir iş anahtarını uygulamamalı veya üzerinde çalışmamalısınız. Bu güzel bir eklenti yani kullanıcılar bu iş anahtarıyla hızlı bir şekilde arama yapabilir, ancak sistem işletim için ona güvenmemelidir.

Birincil anahtara bağlı olarak sorunları var, ama belki de çok önemli değil

Kimlikleri harici sisteme göstermeniz gerekiyorsa, önerdiğim UUID yaklaşımını kullanın. Bunu yapmazsanız, yine de UUID yaklaşımını kullanabilirsiniz, ancak bunu yapmak zorunda değilsiniz. Equals / hashCode içinde DBMS tarafından oluşturulan bir kimliği kullanma sorunu, nesnenin kimliği atamadan önce karma tabanlı koleksiyonlara eklenmiş olabileceği gerçeğinden kaynaklanmaktadır.

Bunu aşmanın en açık yolu, nesneyi, kimliği atamadan önce karma tabanlı koleksiyonlara eklemektir. Bunun her zaman mümkün olmadığını anlıyorum, çünkü kimliği zaten atamadan önce tekilleştirmeyi isteyebilirsiniz. Karma tabanlı koleksiyonları kullanmaya devam etmek için, kimliği atadıktan sonra koleksiyonları yeniden oluşturmanız yeterlidir.

Bunun gibi bir şey yapabilirsiniz:

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

Tam yaklaşımı kendim test etmedim, bu yüzden kalıcılık öncesi ve sonrası olaylarda koleksiyonları nasıl değiştirdiğinizden emin değilim ama fikir:

  • Nesneyi karma tabanlı koleksiyonlardan geçici olarak kaldırın
  • Devam et
  • Nesneyi karma tabanlı koleksiyonlara yeniden ekleyin

Bunu çözmenin bir başka yolu, bir güncelleme / kalıcı işlemden sonra tüm karma tabanlı modellerinizi yeniden oluşturmaktır.

Sonunda, size kalmış. Şahsen sıra tabanlı yaklaşımı çoğu zaman kullanıyorum ve UUID yaklaşımını yalnızca bir tanımlayıcıyı harici sistemlere göstermem gerekirse kullanıyorum.


0

instanceofJava 14'ün yeni stili ile, equalsbir satırda uygulayabilirsiniz .

@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 birçok insanın cevabıysa, neden sadece varlıkları oluşturmak ve oluşturma sırasında birincil anahtarı atamak için iş katmanından fabrika yöntemlerini kullanmıyoruz?

Örneğin:

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

bu şekilde varlık için kalıcılık sağlayıcısından varsayılan bir birincil anahtar alırdık ve hashCode () ve equals () fonksiyonlarımız buna güvenebilirdi.

Ayrıca Aracın inşaatçılarını korumalı ilan edebilir ve daha sonra bunlara erişmek için iş yöntemimizde yansımayı kullanabiliriz. Bu şekilde geliştiriciler Car'ı yeni bir şeyle örneklemek için değil, fabrika yöntemiyle niyet edeceklerdi.

Buna ne dersin?


Bir veritabanı araması yaparken her iki kılavuzu da oluşturmak için performans isabetini almak istiyorsanız harika çalışan bir yaklaşım.
Michael Wiles

1
Birim test Araba hakkında ne? Bu durumda test için bir veritabanı bağlantısına mı ihtiyacınız var? Ayrıca, etki alanı nesnelerinizin kalıcılığa bağlı olmaması gerekir.
jhegedus

-1

Bu soruyu kendim cevaplamaya çalıştım ve bu yazıyı ve özellikle DREW olanı okuyana kadar bulunan çözümlerden asla tamamen memnun kalmadım. Tembel UUID yaratma ve onu en iyi şekilde saklama şeklini beğendim.

Ama daha fazla esneklik eklemek istedim, yani tembel SADECE hashCode () / equals () her çözümün avantajları ile varlığın ilk ısrarından önce erişildiğinde UUID oluşturmak:

  • equals () "nesne aynı mantıksal varlığı ifade eder" anlamına gelir
  • veritabanı kimliğini olabildiğince kullanın çünkü işi neden iki kez yapayım (performans sorunu)
  • henüz var olmayan varlıkta hashCode () / equals () öğesine erişirken sorunu önleyin ve gerçekten devam ettikten sonra aynı davranışı koruyun

Aşağıdaki karışık çözümüm hakkındaki geri bildirimi gerçekten takdir ediyorum

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

"Bu varlık üzerinde kalıcı olmadan önce bir gün üretilen UUID" ile ne demek istiyorsun? lütfen bu davaya bir örnek verebilir misiniz?
jhegedus

atanmış bir nesil türü kullanabilir misiniz? Neden kimlik üretme türü gereklidir? atanmaya göre bir avantajı var mı?
jhegedus

1) yeni bir MyEntity yaparsanız, 2) bir listeye koyarsanız, 3) sonra veritabanına kaydederseniz 4) bu varlığı DB'den geri yüklerseniz ve 5) yüklenen örneğin listede olup olmadığını görmeye çalışırsanız . Benim tahminim öyle olsa bile olmayacak.
jhegedus

Bana gösterdiğim ilk yorumlarınız için teşekkür ederim. Birincisi, "Bu varlık üzerinde ben ısrar önce bir gün üretilen UUID" bir yazım hatasıydı ... "IT devam etmeden önce" yerine okunmalıdır. Diğer açıklamalar için, çözümümü daha iyi açıklamaya çalışmak için yazımı yakında düzenleyeceğim.
user2083808

-1

Uygulamada, Seçenek 2'nin (Birincil anahtar) en sık kullanıldığı görülmektedir. Doğal ve TAŞINMAZ iş anahtarı nadiren bir şeydir, sentetik anahtarlar oluşturmak ve desteklemek, muhtemelen hiç gerçekleşmeyen durumları çözmek için çok ağırdır. Spring-data-jpa AbstractPersistable uygulamasına bir göz atın (tek şey: Hazırda Bekleme uygulaması kullanımı içinHibernate.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;
}

Sadece HashSet / HashMap'te yeni nesneleri değiştirmenin farkında. Aksine, Seçenek 1 ( Objectuygulama devam ) hemen sonra kırılır merge, bu çok yaygın bir durumdur.

İş anahtarınız yoksa ve karma yapısında yeni bir varlığı manipüle etmek için GERÇEK bir gereksiniminiz varsa hashCode, aşağıda Vlad Mihalcea'nın önerildiği gibi sabit olarak geçersiz kılın .


-2

Aşağıda Scala için basit (ve test edilmiş) bir çözüm bulunmaktadır.

  • Bu çözümün, soruda verilen 3 kategoriden hiçbirine uymadığını unutmayın.

  • Tüm Varlıklarım UUIDEntity'nin alt sınıflarıdır, bu yüzden tekrarlama (KURU) ilkesini izlerim.

  • Gerekirse UUID üretimi daha hassas hale getirilebilir (daha fazla sahte rasgele sayılar kullanılarak).

Scala Kodu:

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()
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.