Le dilemme JPA hashCode () / equals ()

Il y a eu quelques discussions à propos des entités JPA et de l’implémentation de hashCode() / equals() pour les classes d’entités JPA. La plupart (sinon la totalité) d’entre eux dépendent d’Hibernate, mais j’aimerais en discuter de manière neutre avec les implémentations JPA (j’utilise d’ailleurs EclipseLink).

Toutes les implémentations possibles présentent leurs propres avantages et inconvénients en ce qui concerne:

  • hashCode() / equals() conformité du contrat (immutabilité) pour les opérations List / Set
  • Si des objects identiques (par exemple, provenant de différentes sessions, des proxies dynamics provenant de structures de données chargées paresseusement) peuvent être détectés
  • Indique si les entités se comportent correctement dans un état détaché (ou non persistant)

Pour autant que je puisse voir, il y a trois options :

  1. Ne les remplacez pas; compter sur Object.equals() et Object.hashCode()
    • hashCode() / equals() travail
    • ne peut pas identifier des objects identiques, des problèmes avec les proxies dynamics
    • pas de problèmes avec des entités détachées
  2. Les remplacer, en fonction de la clé primaire
    • hashCode() / equals() sont rompus
    • identité correcte (pour toutes les entités gérées)
    • problèmes avec des entités détachées
  3. Remplacez-les, en fonction de l’ ID métier (champs de clé non primaires; qu’en est-il des clés étrangères?)
    • hashCode() / equals() sont rompus
    • identité correcte (pour toutes les entités gérées)
    • pas de problèmes avec des entités détachées

Mes questions sont:

  1. Ai-je manqué une option et / ou un avantage / con?
  2. Quelle option avez-vous choisie et pourquoi?

MISE À JOUR 1:

Par ” hashCode() / equals() sont cassés”, je veux dire que les invocations hashCode() successives peuvent renvoyer des valeurs différentes, ce qui est (lorsqu’il est correctement implémenté) pour récupérer une entité modifiée à partir d’une Map , d’un Set ou d’une autre Collection basée sur le hachage. Par conséquent, les implémentations JPA (au moins EclipseLink) ne fonctionneront pas correctement dans certains cas.

MISE À JOUR 2:

Merci pour vos réponses – la plupart d’entre elles ont une qualité remarquable.
Malheureusement, je ne sais toujours pas quelle approche sera la meilleure pour une application réelle ou comment déterminer la meilleure approche pour mon application. Donc, je vais garder la question ouverte et espérer d’autres discussions et / ou opinions.

Lisez ce très bel article sur le sujet: Ne laissez pas Hibernate voler votre identité .

La conclusion de l’article se présente comme suit:

L’identité d’object est difficile à implémenter correctement lorsque des objects sont conservés dans une firebase database. Cependant, les problèmes découlent entièrement de l’autorisation des objects sans identifiant avant leur enregistrement. Nous pouvons résoudre ces problèmes en prenant la responsabilité d’atsortingbuer des identifiants d’object à des frameworks de cartographie relationnelle tels que Hibernate. Au lieu de cela, les ID d’object peuvent être affectés dès que l’object est instancié. Cela rend l’identité de l’object simple et sans erreur, et réduit la quantité de code nécessaire dans le modèle de domaine.

Je remplace toujours égal / hashcode et l’implémente en fonction de l’identifiant métier. Semble la solution la plus raisonnable pour moi. Voir le lien suivant.

Pour résumer tout cela, voici une liste de ce qui fonctionnera ou ne fonctionnera pas avec les différentes manières de gérer les égaux / hashCode: entrer la description de l'image ici

EDIT :

Pour expliquer pourquoi cela fonctionne pour moi:

  1. Je n’utilise généralement pas de collection basée sur le hash (HashMap / HashSet) dans mon application JPA. Si je le dois, je préfère créer une solution UniqueList.
  2. Je pense que modifier un identifiant d’entreprise à l’exécution n’est pas une pratique recommandée pour toute application de firebase database. Dans les rares cas où il n’y a pas d’autre solution, je ferais un traitement spécial comme supprimer l’élément et le remettre dans la collection basée sur le hachage.
  3. Pour mon modèle, j’ai défini l’identifiant d’entreprise sur le constructeur et ne fournit pas de paramètre pour cela. Je laisse l’implémentation de JPA pour changer le champ au lieu de la propriété.
  4. La solution UUID semble exagérée. Pourquoi UUID si vous avez un identifiant d’entreprise naturel? Après tout, je définirais l’unicité de l’identifiant d’entreprise dans la firebase database. Pourquoi avoir TROIS index pour chaque table dans la firebase database alors?

Nous avons généralement deux identifiants dans nos entités:

  1. Est uniquement pour la couche de persistance (afin que le fournisseur de persistance et la firebase database puissent déterminer les relations entre les objects).
  2. Est pour nos besoins d’application ( equals() et hashCode() en particulier)

Regarde:

 @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 Ssortingng screenName; private Ssortingng 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, 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: pour clarifier mon point concernant les appels à la méthode setUuid() . Voici un scénario typique:

 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 

Lorsque je lance mes tests et que je vois la sortie du journal, je résous le problème:

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

Alternativement, on peut fournir un constructeur séparé:

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

Donc, mon exemple ressemblerait à ceci:

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

J’utilise un constructeur et un setter par défaut, mais vous pouvez trouver que l’approche à deux constructeurs vous convient mieux.

Si vous voulez utiliser equals()/hashCode() pour vos ensembles, dans le sens où la même entité ne peut y figurer qu’une seule fois, il n’y a qu’une seule option: Option 2. En effet, une clé primaire pour une entité par définition ne change jamais (si quelqu’un le met à jour, ce n’est plus la même entité)

Vous devriez prendre cela à la lettre: comme vos equals()/hashCode() sont basés sur la clé primaire, vous ne devez pas utiliser ces méthodes tant que la clé primaire n’est pas définie. Donc, vous ne devriez pas mettre d’entités dans le jeu tant qu’elles ne sont pas associées à une clé primaire. (Oui, les UUID et des concepts similaires peuvent aider à atsortingbuer les clés primaires plus tôt.)

Maintenant, il est théoriquement possible d’atteindre cet objective avec l’option 3, même si les “clés métier” ont l’inconvénient de changer: “Tout ce que vous avez à faire est de supprimer les entités déjà insérées dans l’ensemble ( s) et réinsérez-les. ” C’est vrai – mais cela signifie aussi que dans un système dissortingbué, vous devrez vous assurer que cela est fait absolument partout où les données ont été insérées (et vous devrez vous assurer que la mise à jour est effectuée). , avant que d’autres choses ne se produisent). Vous aurez besoin d’un mécanisme de mise à jour sophistiqué, surtout si certains systèmes distants ne sont pas accessibles actuellement …

L’option 1 ne peut être utilisée que si tous les objects de vos jeux proviennent de la même session Hibernate. La documentation d’Hibernate le montre très clairement au chapitre 13.1.3. Considérant l’identité de l’object :

Dans une session, l’application peut utiliser en toute sécurité == pour comparer des objects.

Cependant, une application qui utilise == en dehors d’une session peut produire des résultats inattendus. Cela peut se produire même dans des endroits inattendus. Par exemple, si vous placez deux instances détachées dans le même ensemble, les deux peuvent avoir la même identité de firebase database (elles représentent la même ligne). L’identité de la machine virtuelle Java n’est cependant pas garantie par définition pour les instances dans un état détaché. Le développeur doit remplacer les méthodes equals () et hashCode () dans les classes persistantes et implémenter leur propre notion d’égalité d’object.

Il continue de plaider en faveur de l’option 3:

Il y a une mise en garde: n’utilisez jamais l’identificateur de firebase database pour implémenter l’égalité. Utilisez une clé d’entreprise qui est une combinaison d’atsortingbuts uniques, généralement immuables. L’identifiant de la firebase database changera si un object transitoire est rendu persistant. Si l’instance transitoire (généralement associée à des instances détachées) est contenue dans un ensemble, la modification du code de hachage casse le contrat de l’ensemble.

Ceci est vrai si vous

  • ne peut pas atsortingbuer le numéro rapidement (par exemple en utilisant des UUID)
  • Et pourtant, vous voulez absolument mettre vos objects dans des ensembles pendant qu’ils sont en état transitoire.

Sinon, vous êtes libre de choisir l’option 2.

Ensuite, il mentionne la nécessité d’une stabilité relative:

Les atsortingbuts pour les clés métier ne doivent pas nécessairement être aussi stables que les clés primaires de firebase database; il suffit de garantir la stabilité tant que les objects sont dans le même ensemble.

C’est correct. Le problème pratique que je vois est le suivant: si vous ne pouvez pas garantir une stabilité absolue, comment pourrez-vous garantir la stabilité “tant que les objects sont dans le même ensemble”. Je peux imaginer des cas particuliers (comme utiliser des ensembles uniquement pour une conversation puis les jeter), mais je remettrais en question la praticabilité générale de ceci.


Version courte:

  • L’option 1 ne peut être utilisée qu’avec des objects au sein d’une même session.
  • Si vous le pouvez, utilisez l’option 2. (Atsortingbuez PK le plus tôt possible, car vous ne pouvez pas utiliser les objects dans les ensembles tant que le PK n’est pas atsortingbué.)
  • Si vous pouvez garantir une stabilité relative, vous pouvez utiliser l’option 3. Mais faites attention à cela.

Personnellement, j’ai déjà utilisé toutes ces trois stratégies dans différents projets. Je dois dire que l’option 1 est à mon avis la plus pratique dans une application réelle. Une expérience de rupture de hashCode () / equals () conduit à de nombreux bogues fous, car vous vous retrouverez à chaque fois dans des situations où le résultat de l’égalité change après l’ajout d’une entité à une collection.

Mais il y a d’autres options (également avec leurs avantages et inconvénients):


a) hashCode / est basé sur un ensemble de champs immuables , non nuls , atsortingbués par le constructeur

(+) les trois critères sont garantis

Les valeurs de champ (-) doivent être disponibles pour créer une nouvelle instance

(-) compliquer la manipulation si vous devez en changer un


b) hashCode / est égal à la clé primaire atsortingbuée par l’application (dans le constructeur) au lieu de JPA

(+) les trois critères sont garantis

(-) vous ne pouvez pas tirer parti des stratégies de génération d’identifiants simples et fiables comme les séquences de firebase database

(-) compliqué si de nouvelles entités sont créées dans un environnement dissortingbué (client / serveur) ou un cluster de serveurs d’applications


c) hashCode / est égal à un UUID atsortingbué par le constructeur de l’entité

(+) les trois critères sont garantis

(-) surcharge de génération d’UUID

(-) peut être un petit risque que deux fois le même UUID soit utilisé, en fonction de l’algorithme utilisé (peut être détecté par un index unique sur la firebase database)

Bien que l’utilisation d’une clé métier (option 3) soit l’approche la plus couramment recommandée ( wiki de la communauté Hibernate , “Java Persistence with Hibernate” p. 398), et c’est ce que nous utilisons le plus souvent, il existe un bogue Hibernate ensembles: HHH-3799 . Dans ce cas, Hibernate peut append une entité à un ensemble avant que ses champs ne soient initialisés. Je ne suis pas sûr de savoir pourquoi ce problème n’a pas fait l’object de plus d’attention, car cela rend l’approche par clé métier recommandée problématique.

Je pense que le cœur du problème est que les égaux et hashCode devraient être basés sur un état immuable (référence Odersky et al. ), Et une entité Hibernate avec une clé primaire gérée par Hibernate n’a pas un tel état immuable. La clé primaire est modifiée par Hibernate lorsqu’un object transitoire devient persistant. La clé métier est également modifiée par Hibernate, lorsqu’elle hydrate un object en cours d’initialisation.

Cela ne laisse que l’option 1, héritant des implémentations java.lang.Object basées sur l’identité de l’object, ou utilisant une clé primaire gérée par l’application comme suggéré par James Brundege dans “Ne laissez pas Hibernate voler votre identité” (déjà référencé par la réponse de Stijn Geukens) ) et par Lance Arlaus dans “Object Generation: Une meilleure approche de l’intégration Hibernate” .

Le plus gros problème avec l’option 1 est que les instances détachées ne peuvent pas être comparées aux instances persistantes utilisant .equals (). Mais ça va; le contrat d’égal à égal et de hashCode laisse au développeur le soin de décider quelle égalité signifie pour chaque classe. Donc, laissez simplement equals et hashCode hériter de Object. Si vous devez comparer une instance détachée à une instance persistante, vous pouvez créer une nouvelle méthode explicitement à cette fin, par exemple boolean sameEntity ou boolean dbEquivalent ou boolean businessEquals .

  1. Si vous avez une clé métier , vous devez l’utiliser pour equals / hashCode .
  2. Si vous ne possédez pas de clé métier, vous ne devez pas laisser l’ Object par défaut égal à l’implémentation et à l’implémentation de hashCode car cela ne fonctionne pas après la merge et l’entité.
  3. Vous pouvez utiliser l’identifiant d’entité comme suggéré dans cet article . Le seul problème est que vous devez utiliser une implémentation hashCode qui renvoie toujours la même valeur, comme ceci:

     @Entity public class Book implements Identifiable { @Id @GeneratedValue private Long id; private Ssortingng 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 } 

Je suis d’accord avec la réponse d’Andrew. Nous faisons la même chose dans notre application mais au lieu de stocker les UUID comme VARCHAR / CHAR, nous la divisons en deux longues valeurs. Voir UUID.getLeastSignificantBits () et UUID.getMostSignificantBits ().

Une autre chose à prendre en compte est que les appels à UUID.randomUUID () sont assez lents, vous pouvez donc envisager de générer paresseusement l’UUID uniquement lorsque cela est nécessaire, comme lors de la persistance ou des appels à 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 dissortingbuted 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(); } } 

Comme d’autres personnes ont été beaucoup plus intelligentes que moi, il existe de nombreuses stratégies. Il semble que la majorité des modèles de conception appliqués tentent de réussir. Ils limitent l’access des constructeurs, sinon empêchent complètement les invocations des constructeurs avec des constructeurs spécialisés et des méthodes d’usine. En effet, il est toujours agréable avec une API claire. Mais si la seule raison est de faire en sorte que les substitutions de hashcode et d’égale soient compatibles avec l’application, alors je me demande si ces stratégies sont en conformité avec KISS (Keep It Simple Stupid).

Pour moi, j’aime écraser les égaux et le hashcode en examinant l’identifiant. Dans ces méthodes, je demande que l’identifiant ne soit pas nul et documente bien ce comportement. Ainsi, il deviendra le contrat du développeur de conserver une nouvelle entité avant de le stocker ailleurs. Une application qui n’honore pas ce contrat échouerait dans la minute (espérons-le).

Attention toutefois: si vos entités sont stockées dans des tables différentes et que votre fournisseur utilise une stratégie de génération automatique pour la clé primaire, vous obtiendrez des clés primaires dupliquées sur tous les types d’entités. Dans ce cas, comparez également les types d’exécution avec un appel à Object # getClass (), ce qui, bien sûr, empêchera deux types différents d’être considérés comme égaux. Cela me va très bien pour la plupart.

Il y a évidemment déjà des réponses très instructives ici, mais je vais vous dire ce que nous faisons.

Nous ne faisons rien (c.-à-d. Ne pas surpasser).

If we do need equals/hashcode to work for collections we use UUIDs. You just create the UUID in the constructor. We use http://wiki.fasterxml.com/JugHome for UUID. UUID is a little more expensive CPU wise but is cheap compared to serialization and db access.

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. Avantages:

  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

Les inconvénients:

  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. Quoi d’autre?

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 Ssortingng kind; public AbstractPersistable(final Ssortingng 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:

  • simplicité
  • ensures subclasses provide type identity
  • predicted behavior with proxied classes

Désavantages:

  • Requires each entity to call super()

Remarques:

  • 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

Looking forward to your comments.

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?

par exemple:

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