Il dilemma di hashCode () / equals () di JPA

Ci sono state alcune discussioni qui sulle quadro JPA e quale implementazione hashCode() / equals() dovrebbe essere usata per le classi di quadro JPA. La maggior parte (se non tutti) di loro dipendono da Hibernate, ma mi piacerebbe discuterne in modo neutro l’implementazione JPA (sto usando EclipseLink, tra l’altro).

Tutte le possibili implementazioni hanno i loro vantaggi e svantaggi rispetto a:

  • hashCode() / equals() conformità del contratto (immutabilità) per le operazioni List / Set
  • È ansible rilevare oggetti identici (ad es. Da sessioni diverse, proxy dinamici da strutture di dati caricate pigramente)
  • Se le quadro si comportano correttamente nello stato staccato (o non persistente)

Per quanto posso vedere, ci sono tre opzioni :

  1. Non ignorarli; fare affidamento su Object.equals() e Object.hashCode()
    • hashCode() / equals() funzionano
    • imansible identificare oggetti identici, problemi con i proxy dinamici
    • nessun problema con quadro distaccate
  2. Sovrascrivi, in base alla chiave primaria
    • hashCode() / equals() sono rotti
    • identity framework corretta (per tutte le quadro gestite)
    • problemi con quadro distaccate
  3. Sovrascriverli, in base all’ID business (campi chiave non primari, a proposito di chiavi esterne?)
    • hashCode() / equals() sono rotti
    • identity framework corretta (per tutte le quadro gestite)
    • nessun problema con entity framework distaccate

Le mie domande sono:

  1. Ho perso un’opzione e / o un punto di pro / contro?
  2. Quale opzione hai scelto e perché?

AGGIORNAMENTO 1:

Con ” hashCode() / equals() sono interrotti”, voglio dire che successive chiamate a hashCode() possono restituire valori diversi, che è (se correttamente implementato) non è rotto nel senso della documentazione API Object , ma che causa problemi quando si prova per recuperare un’entity framework modificata da una Map , Set o altra Collection basata su hash. Di conseguenza, le implementazioni JPA (almeno EclipseLink) non funzioneranno correttamente in alcuni casi.

AGGIORNAMENTO 2:

Grazie per le vostre risposte – la maggior parte ha una qualità notevole.
Sfortunatamente, non sono ancora sicuro quale approccio sia il migliore per un’applicazione reale o come determinare l’approccio migliore per la mia applicazione. Quindi terrò aperta la domanda e spero in ulteriori discussioni e / o opinioni.

Leggi questo bellissimo articolo sull’argomento: Non lasciare che Hibernate rubi la tua id quadro .

La conclusione dell’articolo è così:

L’id quadro dell’object è ingannevolmente difficile da implementare correttamente quando gli oggetti vengono mantenuti su un database. Tuttavia, i problemi derivano interamente dal permettere agli oggetti di esistere senza un ID prima che vengano salvati. Siamo in grado di risolvere questi problemi assumendo la responsabilità di assegnare gli ID object lontano dai framework di mapping relazionali agli oggetti come Hibernate. Invece, gli ID object possono essere assegnati non appena l’istanza dell’object viene istanziata. Ciò rende l’id quadro dell’object semplice e priva di errori e riduce la quantità di codice necessaria nel modello di dominio.

Sostituisco sempre equals / hashcode e lo implemento in base all’ID aziendale. Sembra la soluzione più ragionevole per me. Vedi il seguente link .

Per riassumere tutto questo, ecco un elenco di ciò che funzionerà o non funzionerà con i diversi modi di gestire equals / hashCode: inserisci la descrizione dell'immagine qui

MODIFICA :

Per spiegare perché questo funziona per me:

  1. Di solito non utilizzo la raccolta basata su hash (HashMap / HashSet) nella mia applicazione JPA. Se devo, preferisco creare la soluzione UniqueList.
  2. Penso che cambiare l’ID di business in runtime non sia una best practice per qualsiasi applicazione di database. Nei rari casi in cui non ci sono altre soluzioni, farei un trattamento speciale come rimuovere l’elemento e rimetterlo nella raccolta basata su hash.
  3. Per il mio modello, ho impostato l’id aziendale sul costruttore e non fornisco setter per questo. Lascio l’implementazione JPA per modificare il campo anziché la proprietà.
  4. La soluzione UUID sembra essere eccessiva. Perché UUID se hai un business id naturale? Dopo tutto, avrei impostato l’univocità dell’ID aziendale nel database. Perché avere TRE indici per ogni tabella nel database, quindi?

Di solito abbiamo due ID nelle nostre quadro:

  1. È solo per il livello di persistenza (in modo che il provider di persistenza e il database possano capire le relazioni tra gli oggetti).
  2. È per le nostre esigenze applicative ( equals() e hashCode() in particolare)

Guarda:

 @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 classs 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, ie 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: per chiarire il mio punto riguardante le chiamate al metodo setUuid() . Ecco uno scenario tipico:

 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 

Quando eseguo i test e visualizzo l’output del registro, risolvo il problema:

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

In alternativa, è ansible fornire un costruttore separato:

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

Quindi il mio esempio dovrebbe assomigliare a questo:

 User user = new User(UUID.randomUUID()); ... jediSet.add(user); // no bug this time em.persist(user); // and no log output 

Io uso un costruttore di default e un setter, ma potresti trovare l’approccio di due costruttori più adatto a te.

Se vuoi usare equals()/hashCode() per i tuoi Sets, nel senso che la stessa quadro può essere lì solo una volta, allora c’è solo un’opzione: Opzione 2. Questo perché una chiave primaria per un’ quadro per definizione non cambia mai (se qualcuno lo aggiorna davvero, non è più la stessa entity framework)

Dovresti prenderlo alla lettera: poiché i tuoi equals()/hashCode() sono basati sulla chiave primaria, non devi usare questi metodi, finché non viene impostata la chiave primaria. Quindi non dovresti mettere le quadro nel set, finché non le viene assegnata una chiave primaria. (Sì, UUID e concetti simili possono aiutare ad assegnare le chiavi primarie in anticipo.)

Ora, in teoria è anche ansible ottenerlo con l’Opzione 3, anche se i cosiddetti “business-key” hanno il cattivo inconveniente che possono cambiare: “Tutto quello che devi fare è eliminare le entity framework già inserite dal set ( s) e reinserirli. ” Questo è vero, ma significa anche che, in un sistema distribuito, dovrai assicurarti che questo avvenga assolutamente ovunque i dati siano stati inseriti (e dovrai assicurarti che l’aggiornamento sia eseguito , prima che si verifichino altre cose). Avrai bisogno di un sofisticato meccanismo di aggiornamento, specialmente se alcuni sistemi remoti non sono attualmente raggiungibili …

L’opzione 1 può essere utilizzata solo se tutti gli oggetti nei set appartengono alla stessa sessione di sospensione. La documentazione di Hibernate lo rende molto chiaro nel capitolo 13.1.3. Considerando l’id quadro dell’object :

All’interno di una sessione l’applicazione può tranquillamente usare == per confrontare gli oggetti.

Tuttavia, un’applicazione che utilizza == al di fuori di una sessione potrebbe produrre risultati imprevisti. Questo potrebbe accadere anche in alcuni posti inaspettati. Ad esempio, se metti due istanze distaccate nello stesso Set, entrambe potrebbero avere la stessa id quadro del database (cioè rappresentano la stessa riga). L’id quadro JVM, tuttavia, per definizione non è garantita per le istanze in uno stato di distacco. Lo sviluppatore deve scavalcare i metodi equals () e hashCode () nelle classi persistenti e implementare la propria nozione di uguaglianza degli oggetti.

Continua a sostenere l’opzione 3:

C’è un avvertimento: non utilizzare mai l’identificatore del database per implementare l’uguaglianza. Utilizzare una chiave aziendale che sia una combinazione di attributi unici, solitamente immutabili. L’identificatore del database cambierà se un object temporaneo viene reso persistente. Se l’istanza transitoria (di solito insieme alle istanze distaccate) è contenuta in un Set, la modifica dell’hashcode interrompe il contratto del Set.

Questo è vero, se tu

  • non posso assegnare l’id in anticipo (es. usando UUID)
  • eppure vuoi assolutamente mettere i tuoi oggetti in set mentre sono in stato transitorio.

Altrimenti, sei libero di scegliere l’Opzione 2.

Quindi menziona la necessità di una stabilità relativa:

Gli attributi per le chiavi aziendali non devono essere stabili come le chiavi primarie del database; devi solo garantire la stabilità finché gli oggetti si trovano nello stesso Set.

Questo è corretto. Il problema pratico che vedo con questo è: se non puoi garantire la stabilità assoluta, come sarai in grado di garantire la stabilità “fintanto che gli oggetti si trovano nello stesso Set”. Posso immaginare alcuni casi speciali (come usare set solo per una conversazione e poi buttarli via), ma metterei in discussione la praticabilità generale di questo.


Versione breve:

  • L’opzione 1 può essere utilizzata solo con oggetti all’interno di una singola sessione.
  • Se puoi, usa l’opzione 2. (Assegna PK il prima ansible, perché non puoi usare gli oggetti nei set finché non viene assegnato il PK.)
  • Se puoi garantire la stabilità relativa, puoi usare l’Opzione 3. Ma fai attenzione a questo.

Personalmente ho già utilizzato tutte queste tre strategie in diversi progetti. An Devo dire che l’opzione 1 è secondo me la più praticabile in un’app di vita reale. A ha reso l’esperienza che rompe la conformità hashCode () / equals () porta a molti bug pazzi, come ogni volta che finisci in situazioni in cui il risultato dell’eguaglianza cambia dopo che un’ quadro è stata aggiunta a una raccolta.

Ma ci sono ulteriori opzioni (anche con i loro pro e contro):


a) hashCode / equals basato su un insieme di campi immutabili , non nulli , assegnati dal costruttore

(+) tutti e tre i criteri sono garantiti

(-) i valori dei campi devono essere disponibili per creare una nuova istanza

(-) complicare la gestione se è necessario modificare uno dei seguenti


b) hashCode / equals basato sulla chiave primaria che viene assegnata dall’applicazione (nel costruttore) al posto di JPA

(+) tutti e tre i criteri sono garantiti

(-) non è ansible sfruttare le semplici strategie di generazione ID affidabili come le sequenze DB

(-) complicato se vengono create nuove quadro in un ambiente distribuito (client / server) o cluster di server delle app


c) hashCode / equals basato su un UUID assegnato dal costruttore dell’entity framework

(+) tutti e tre i criteri sono garantiti

(-) sovraccarico della generazione di UUID

(-) potrebbe essere un piccolo rischio che venga utilizzato il doppio UUID, a seconda dell’algoritmo utilizzato (può essere rilevato da un indice univoco su DB)

Sebbene l’uso di una chiave aziendale (opzione 3) sia l’approccio più comunemente raccomandato ( wiki della comunità di Hibernate , “Persistenza di Java con Hibernate” a pagina 398), e questo è quello che usiamo maggiormente, c’è un bug di Hibernate che lo interrompe con impazienza set: HHH-3799 . In questo caso, Hibernate può aggiungere un’ quadro a un set prima che i suoi campi siano inizializzati. Non sono sicuro del motivo per cui questo bug non ha ottenuto più attenzione, poiché rende davvero problematico l’approccio business-key raccomandato.

Penso che il nocciolo della questione sia che uguaglianza e hashCode dovrebbero essere basati sullo stato immutabile (riferimento Odersky et al. ), E un’ quadro di Hibernate con la chiave primaria gestita da Hibernate non ha uno stato immutabile. La chiave primaria viene modificata da Hibernate quando un object transitorio diventa persistente. La chiave aziendale viene anche modificata da Hibernate, quando idrata un object nel processo di inizializzazione.

Ciò lascia solo l’opzione 1, ereditando le implementazioni java.lang.Object basate sull’identity framework dell’object, o usando una chiave primaria gestita dall’applicazione come suggerito da James Brundege in “Non lasciare che Hibernate rubi la tua id quadro” (già referenziato dalla risposta di Stijn Geukens ) e di Lance Arlaus in “Generazione di oggetti: un approccio migliore per l’integrazione in ibernazione” .

Il più grande problema con l’opzione 1 è che le istanze distaccate non possono essere confrontate con istanze persistenti usando .equals (). Ma va bene; il contratto di pari e hashCode lascia allo sviluppatore la decisione su cosa significhi l’uguaglianza per ogni class. Quindi lascia che siano uguali e hashCode erediti da Object. Se è necessario confrontare un’istanza distaccata con un’istanza persistente, è ansible creare un nuovo metodo esplicitamente a tale scopo, ad esempio boolean sameEntity o boolean dbEquivalent o boolean businessEquals .

  1. Se hai una chiave aziendale , dovresti usarla per equals / hashCode .
  2. Se non hai una chiave aziendale, non dovresti lasciarla con le implementazioni Object equals e hash di default perché non funziona dopo l’ merge e l’ quadro.
  3. Puoi utilizzare l’identificatore di quadro come suggerito in questo post . L’unico problema è che è necessario utilizzare un’implementazione hashCode che restituisce sempre lo stesso valore, come questo:

     @Entity public class Book implements Identifiable { @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 } 

Sono d’accordo con la risposta di Andrew. Facciamo la stessa cosa nella nostra applicazione, ma invece di archiviare gli UUID come VARCHAR / CHAR, li dividiamo in due lunghi valori. Vedere UUID.getLeastSignificantBits () e UUID.getMostSignificantBits ().

Un’altra cosa da considerare, è che le chiamate a UUID.randomUUID () sono piuttosto lente, quindi potresti voler esaminare pigramente la generazione dell’UUID solo quando necessario, come durante la persistenza o le chiamate a 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(); } } 

Come altre persone in modo più intelligente di me ha già sottolineato, c’è una grande quantità di strategie là fuori. Sembra tuttavia che la maggior parte dei modelli di design applicati provino a farsi strada verso il successo. Limitano l’accesso al costruttore se non ostacolano completamente le chiamate del costruttore con costruttori specializzati e metodi di fabbrica. Effettivamente è sempre piacevole con un’API chiara. Ma se l’unico motivo è quello di rendere le sostituzioni uguali e hashcode compatibili con l’applicazione, allora mi chiedo se tali strategie siano conformi a KISS (Keep It Simple Stupid).

Per me, mi piace sovrascrivere equals e hashcode esaminando l’id. In questi metodi, richiedo che l’id non sia null e documenta bene questo comportamento. Così diventerà il contratto degli sviluppatori per mantenere una nuova entity framework prima di immagazzinarlo altrove. Un’applicazione che non rispetta questo contratto fallirebbe nel giro di minuto (si spera).

Un avvertimento: se le tue entity framework sono archiviate in tabelle diverse e il tuo provider utilizza una strategia di generazione automatica per la chiave primaria, otterrai chiavi duplicate primarie tra i tipi di entity framework. In tal caso, confrontare anche i tipi di tempo di esecuzione con una chiamata a Object # getClass () che ovviamente renderà imansible che due diversi tipi siano considerati uguali. Mi va bene per la maggior parte.

Ovviamente ci sono già risposte molto istruttive, ma ti dirò cosa facciamo.

Non facciamo nulla (cioè non escludiamo).

Se abbiamo bisogno di equals / hashcode per lavorare per le collezioni usiamo UUID. Devi solo creare l’UUID nel costruttore. Usiamo http://wiki.fasterxml.com/JugHome per UUID. UUID è un po ‘più costoso della CPU, ma è economico rispetto alla serializzazione e all’accesso db.

Business keys approach doesn’t suit for us. We use DB generated ID , temporary transient tempId and override equal()/hashcode() to solve the dilemma. All entities are descendants of Entity. Professionisti:

  1. No extra fields in DB
  2. No extra coding in descendants entities, one approach for all
  3. No performance issues (like with UUID), DB Id generation
  4. No problem with Hashmaps (don’t need to keep in mind the use of equal & etc.)
  5. Hashcode of new entity doesn’t changed in time even after persisting

Contro:

  1. There are may be problems with serializing and deserializing not persisted entities
  2. Hashcode of the saved entity may change after reloading from DB
  3. Not persisted objects considered always different (maybe this is right?)
  4. Cos’altro?

Look at our code:

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

Please consider the following approach based on predefined type identifier and the ID.

The specific assumptions for JPA:

  • entities of the same “type” and the same non-null ID are considered equal
  • non-persisted entities (assuming no ID) are never equal to other entities

The abstract entity:

 @MappedSuperclass public abstract class AbstractPersistable { @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; } } 

Concrete entity example:

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

Test example:

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

Main advantages here:

  • semplicità
  • ensures subclasss provide type identity
  • predicted behavior with proxied classs

svantaggi:

  • Requires each entity to call super()

Gli appunti:

  • Needs attention when using inheritance. Eg instance equality of class A and class B extends A may depend on concrete details of the application.
  • Ideally, use a business key as the ID

In attesa di vostri commenti.

I have always used option 1 in the past because I was aware of these discussions and thought it was better to do nothing until I knew the right thing to do. Those systems are all still running successfully.

However, next time I may try option 2 – using the database generated Id.

Hashcode and equals will throw IllegalStateException if the id is not set.

This will prevent subtle errors involving unsaved entities from appearing unexpectedly.

What do people think of this approach?

This is a common problem in every IT system that uses Java and JPA. The pain point extends beyond implementing equals() and hashCode(), it affects how an organization refer to an entity and how its clients refer to the same entity. I’ve seen enough pain of not having a business key to the point that I wrote my own blog to express my view.

In short: use a short, human readable, sequential ID with meaningful prefixes as business key that’s generated without any dependency on any storage other than RAM. Twitter’s Snowflake is a very good example.

If UUID is the answer for many people, why don’t we just use factory methods from business layer to create the entities and assign primary key at creation time?

per esempio:

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

this way we would get a default primary key for the entity from the persistence provider, and our hashCode() and equals() functions could rely on that.

We could also declare the Car’s constructors protected and then use reflection in our business method to access them. This way developers would not be intent on instantiate Car with new, but through factory method.

How’bout that?

I tried to answer this question myself and was never totally happy with found solutions until i read this post and especially DREW one. I liked the way he lazy created UUID and optimally stored it.

But I wanted to add even more flexibility, ie lazy create UUID ONLY when hashCode()/equals() is accessed before first persistence of the entity with each solution’s advantages :

  • equals() means “object refers to the same logical entity”
  • use database ID as much as possible because why would I do the work twice (performance concern)
  • prevent problem while accessing hashCode()/equals() on not yet persisted entity and keep the same behaviour after it is indeed persisted

I would really apreciate feedback on my mixed-solution below

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

In practice it seems, that Option 2 (Primary key) is most frequently used. Natural and IMMUTABLE business key is seldom thing, creating and supporting synthetic keys are too heavy to solve situations, which are probably never happened. Have a look at spring-data-jpa AbstractPersistable implementation (the only thing: for Hibernate implementation use Hibernate.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; } 

Just aware of manipulating new objects in HashSet/HashMap. In opposite, the Option 1 (remain Object implementation) is broken just after merge , that is very common situation.

If you have no business key and have a REAL needs to manipulate new entity in hash structure, override hashCode to constant, as below Vlad Mihalcea was advised.

Below is a simple (and tested) solution for Scala.

  • Note that this solution does not fit into any of the 3 categories given in the question.

  • All my Entities are subclasss of the UUIDEntity so I follow the don’t-repeat-yourself (DRY) principle.

  • If needed the UUID generation can be made more precise (by using more pseudo-random numbers).

Scala Code:

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