El dilema de hashCode () / equals () de JPA

Hubo algunas discusiones aquí sobre las entidades JPA y qué hashCode() / equals() debería usarse para las clases de entidad JPA. La mayoría (si no todos) de ellos dependen de Hibernate, pero me gustaría hablar sobre su implementación de JPA de forma neutral (estoy usando EclipseLink, por cierto).

Todas las implementaciones posibles tienen sus propias ventajas y desventajas con respecto a:

  • hashCode() contrato hashCode() / equals() (inmutabilidad) para las operaciones List / Set
  • Si se pueden detectar objetos idénticos (por ejemplo, de diferentes sesiones, proxies dynamics de estructuras de datos cargados de forma ocasional)
  • Si las entidades se comportan correctamente en estado separado (o no persistente)

Hasta donde puedo ver, hay tres opciones :

  1. No los anules; confía en Object.equals() y Object.hashCode()
    • hashCode() / equals() trabajo
    • no puede identificar objetos idénticos, problemas con proxies dynamics
    • sin problemas con entidades separadas
  2. Anularlos, basado en la clave primaria
    • hashCode() / equals() están rotos
    • identidad correcta (para todas las entidades administradas)
    • problemas con entidades separadas
  3. Anularlos, basado en el Business-Id (campos clave no primarios, ¿qué pasa con las claves externas?)
    • hashCode() / equals() están rotos
    • identidad correcta (para todas las entidades administradas)
    • sin problemas con entidades separadas

Mis preguntas son:

  1. ¿Perdí una opción y / o punto pro / con?
  2. ¿Qué opción elegiste y por qué?

ACTUALIZACIÓN 1:

Si ” hashCode() / equals() están rotos”, quiero decir que las sucesivas invocaciones de hashCode() pueden devolver diferentes valores, que (cuando se implementan correctamente) no se rompen en el sentido de la documentación de Object API, pero que causan problemas al intentar para recuperar una entidad modificada de un Map , Set u otra Collection basada en hash. En consecuencia, las implementaciones de JPA (al menos EclipseLink) no funcionarán correctamente en algunos casos.

ACTUALIZACIÓN 2:

Gracias por sus respuestas, la mayoría de ellas tienen una calidad notable.
Lamentablemente, todavía no estoy seguro de qué enfoque será el mejor para una aplicación de la vida real, o cómo determinar el mejor enfoque para mi aplicación. Por lo tanto, voy a mantener abierta la pregunta y espero tener más discusiones y / u opiniones.

Lee este artículo muy bueno sobre el tema: No dejes que Hibernate robe tu identidad .

La conclusión del artículo es la siguiente:

La identidad del objeto es engañosamente difícil de implementar correctamente cuando los objetos se conservan en una base de datos. Sin embargo, los problemas se derivan por completo de permitir que los objetos existan sin una identificación antes de que se guarden. Podemos resolver estos problemas asumiendo la responsabilidad de asignar identificadores de objeto lejos de marcos de mapeo relacionales de objetos como Hibernate. En cambio, los ID de objetos se pueden asignar tan pronto como se crea una instancia del objeto. Esto hace que la identidad del objeto sea simple y sin errores, y reduce la cantidad de código necesario en el modelo de dominio.

Siempre anulo equals / hashcode y lo implemento en función de la identificación comercial. Parece la solución más razonable para mí. Vea el siguiente enlace .

Para resumir todo esto, aquí hay una lista de lo que funcionará o no con las diferentes formas de manejar equals / hashCode: enter image description here

EDITAR :

Para explicar por qué esto funciona para mí:

  1. Normalmente no utilizo la recolección basada en hash (HashMap / HashSet) en mi aplicación JPA. Si debo hacerlo, prefiero crear la solución UniqueList.
  2. Creo que cambiar la id empresarial en el tiempo de ejecución no es una mejor práctica para cualquier aplicación de base de datos. En casos raros donde no hay otra solución, haría un tratamiento especial como eliminar el elemento y volver a ponerlo en la colección basada en hash.
  3. Para mi modelo, establecí el id empresarial en el constructor y no proporciono setters para él. Permití que la implementación de JPA cambiara el campo en lugar de la propiedad.
  4. La solución UUID parece ser excesiva. ¿Por qué UUID si tiene una identificación empresarial natural? Después de todo, establecería la singularidad de la identificación comercial en la base de datos. ¿Por qué entonces tener TRES índices para cada tabla en la base de datos?

Por lo general, tenemos dos identificaciones en nuestras entidades:

  1. Es solo para la capa de persistencia (para que el proveedor de persistencia y la base de datos puedan descubrir las relaciones entre los objetos).
  2. Es para nuestras necesidades de aplicaciones ( equals() y hashCode() en particular)

Echar un vistazo:

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

EDITAR: para aclarar mi punto con respecto a las llamadas al método setUuid() . Aquí hay un escenario típico:

 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 

Cuando ejecuto mis pruebas y veo el resultado del registro, soluciono el problema:

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

Alternativamente, uno puede proporcionar un constructor separado:

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

Entonces mi ejemplo sería así:

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

Utilizo un constructor por defecto y un setter, pero puede encontrar el enfoque de dos constructores más adecuado para usted.

Si quiere usar equals()/hashCode() para sus Sets, en el sentido de que la misma entidad solo puede estar ahí una vez, entonces solo hay una opción: Opción 2. Eso es porque una clave primaria para una entidad por definición nunca cambia (si alguien realmente lo actualiza, ya no es la misma entidad)

Debe tomar esto literalmente: dado que su equals()/hashCode() se basa en la clave principal, no debe usar estos métodos, hasta que se establezca la clave principal. Entonces no debes poner entidades en el conjunto, hasta que se les asigne una clave primaria. (Sí, los UUID y conceptos similares pueden ayudar a asignar claves primarias anticipadamente).

Ahora, teóricamente también es posible lograrlo con la Opción 3, aunque las llamadas “claves de negocios” tienen el desagradable inconveniente de que pueden cambiar: “Todo lo que tendrá que hacer es eliminar las entidades ya insertadas del conjunto ( s), y volver a insertarlos “. Eso es cierto, pero también significa que, en un sistema distribuido, deberá asegurarse de que esto se haga en cualquier lugar donde se hayan insertado los datos (y deberá asegurarse de que se realice la actualización). , antes de que ocurran otras cosas). Necesitará un mecanismo de actualización sofisticado, especialmente si algunos sistemas remotos no son accesibles actualmente …

La opción 1 solo se puede usar si todos los objetos en sus conjuntos pertenecen a la misma sesión de Hibernate. La documentación de Hibernate lo deja muy claro en el capítulo 13.1.3. Considerando la identidad del objeto :

Dentro de una sesión, la aplicación puede usar == para comparar objetos de forma segura.

Sin embargo, una aplicación que usa == fuera de una sesión puede producir resultados inesperados. Esto puede ocurrir incluso en algunos lugares inesperados. Por ejemplo, si coloca dos instancias separadas en el mismo conjunto, ambas pueden tener la misma identidad de base de datos (es decir, representan la misma fila). Sin embargo, la identidad de JVM no está garantizada para las instancias en un estado separado. El desarrollador tiene que anular los métodos equals () y hashCode () en clases persistentes e implementar su propia noción de igualdad de objetos.

Sigue argumentando a favor de la Opción 3:

Hay una advertencia: nunca use el identificador de la base de datos para implementar la igualdad. Use una clave comercial que sea una combinación de atributos únicos, generalmente inmutables. El identificador de la base de datos cambiará si un objeto transitorio se vuelve persistente. Si la instancia transitoria (generalmente junto con instancias separadas) se mantiene en un conjunto, al cambiar el código hash se rompe el contrato del conjunto.

Esto es cierto, si

  • no se puede asignar el ID temprano (por ejemplo, mediante el uso de UUID)
  • y sin embargo, absolutamente desea colocar sus objetos en conjuntos mientras están en estado transitorio.

De lo contrario, puedes elegir la Opción 2.

Luego menciona la necesidad de una estabilidad relativa:

Los atributos para claves de negocios no tienen que ser tan estables como las claves primarias de la base de datos; solo debe garantizar la estabilidad siempre que los objetos estén en el mismo Conjunto.

Esto es correcto. El problema práctico que veo con esto es: si no puede garantizar la estabilidad absoluta, ¿cómo podrá garantizar la estabilidad “mientras los objetos estén en el mismo conjunto”? Me puedo imaginar algunos casos especiales (como usar sets solo para una conversación y luego tirarlos), pero cuestionaría la viabilidad general de esto.


Version corta:

  • La opción 1 solo se puede usar con objetos en una sola sesión.
  • Si puede, use la Opción 2. (Asigne PK lo antes posible, porque no puede usar los objetos en conjuntos hasta que se haya asignado PK).
  • Si puede garantizar la estabilidad relativa, puede usar la Opción 3. Pero tenga cuidado con esto.

Personalmente ya usé todas estas tres estrategias en diferentes proyectos. Y debo decir que la opción 1 es, en mi opinión, la más practicable en una aplicación de la vida real. A hizo que la experiencia de romper la conformidad de hashCode () / equals () condujera a muchos errores locos, ya que cada vez terminarás en situaciones en las que el resultado de la igualdad cambia después de que una entidad se haya agregado a una colección.

Pero hay más opciones (también con sus pros y sus contras):


a) hashCode / equals basado en un conjunto de campos inmutables , no nulos , asignados por el constructor

(+) los tres criterios están garantizados

(-) los valores de campo deben estar disponibles para crear una nueva instancia

(-) complicar el manejo si debe cambiar uno de los siguientes


b) hashCode / equals basado en la clave primaria asignada por la aplicación (en constructor) en lugar de JPA

(+) los tres criterios están garantizados

(-) no puede aprovechar las estrategias de generación de ID simples y fiables, como las secuencias de DB

(-) complicado si se crean nuevas entidades en un entorno distribuido (cliente / servidor) o clúster de servidores de aplicaciones


c) hashCode / equals basado en un UUID asignado por el constructor de la entidad

(+) los tres criterios están garantizados

(-) sobrecarga de la generación de UUID

(-) puede existir un pequeño riesgo de que se use el doble del mismo UUID, dependiendo del algoritmo utilizado (puede detectarse mediante un índice único en DB)

Aunque el uso de una clave de negocios (opción 3) es el enfoque más recomendado ( wiki de la comunidad de Hibernate , “Persistencia de Java con Hibernate”, página 398), y esto es lo que más utilizamos, hay un error de Hibernate que rompe esto por ansioso juegos: HHH-3799 . En este caso, Hibernate puede agregar una entidad a un conjunto antes de que se inicialicen sus campos. No estoy seguro de por qué este error no ha recibido más atención, ya que realmente hace que el enfoque recomendado de clave empresarial sea problemático.

Creo que el meollo de la cuestión es que equals y hashCode deben basarse en el estado inmutable (referencia Odersky et al. ), Y una entidad Hibernate con clave principal gestionada por Hibernate no tiene tal estado inmutable. La clave primaria es modificada por Hibernate cuando un objeto transitorio se vuelve persistente. La clave de negocio también es modificada por Hibernate, cuando hidrata un objeto en el proceso de inicialización.

Eso deja solo la opción 1, heredando las implementaciones java.lang.Object basadas en la identidad del objeto, o usando una clave primaria administrada por la aplicación como sugiere James Brundege en “Do not Let Hibernate Steal Your Identity” (ya mencionado por la respuesta de Stijn Geukens ) y por Lance Arlaus en “Object Generation: A Better Approach to Hibernate Integration” .

El mayor problema con la opción 1 es que las instancias separadas no se pueden comparar con las instancias persistentes usando .equals (). Pero eso esta bien; el contrato de iguales y hashCode le deja al desarrollador decidir qué significa igualdad para cada clase. Así que simplemente deje que equals y hashCode hereden de Object. Si necesita comparar una instancia separada con una instancia persistente, puede crear un nuevo método explícitamente para tal fin, tal vez boolean sameEntity o boolean dbEquivalent o boolean businessEquals .

  1. Si tiene una clave comercial , entonces debe usarla para equals / hashCode .
  2. Si no tiene una clave comercial, no debe dejarla con las implementaciones predeterminadas de Object equals y hashCode porque eso no funciona después de la merge y la entidad.
  3. Puede usar el identificador de entidad como se sugiere en esta publicación . El único hashCode es que debe usar una implementación hashCode que siempre devuelva el mismo valor, así:

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

Estoy de acuerdo con la respuesta de Andrew. Hacemos lo mismo en nuestra aplicación, pero en lugar de almacenar UUID como VARCHAR / CHAR, lo dividimos en dos valores largos. Consulte UUID.getLeastSignificantBits () y UUID.getMostSignificantBits ().

Una cosa más a tener en cuenta es que las llamadas a UUID.randomUUID () son bastante lentas, por lo que es posible que desee considerar la creación lenta del UUID solo cuando sea necesario, como durante la persistencia o llamadas 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(); } } 

Como ya han señalado otras personas mucho más inteligentes que yo, existe una gran cantidad de estrategias. Sin embargo, parece ser el caso que la mayoría de los patrones de diseño aplicados intentan abrirse camino hacia el éxito. Limitan el acceso de los constructores si no obstaculizan por completo las invocaciones de los constructores con constructores especializados y métodos de fábrica. De hecho, siempre es agradable con un API claro. Pero si la única razón es hacer que las anulaciones de igual y código hash sean compatibles con la aplicación, entonces me pregunto si esas estrategias están en conformidad con KISS (Keep It Simple Stupid).

Para mí, me gusta anular equals y hashcode mediante el examen de la identificación. En estos métodos, solicito que la identificación no sea nula y documente bien este comportamiento. Por lo tanto, se convertirá en el contrato de los desarrolladores para persistir en una nueva entidad antes de almacenarlo en otro lugar. Una aplicación que no respeta este contrato fallará dentro del minuto (con suerte).

Sin embargo, tenga cuidado: si sus entidades están almacenadas en tablas diferentes y su proveedor utiliza una estrategia de generación automática para la clave principal, obtendrá claves primarias duplicadas en todos los tipos de entidad. En tal caso, también compare los tipos de tiempo de ejecución con una llamada al Objeto # getClass () que, por supuesto, hará imposible que dos tipos diferentes se consideren iguales. Eso me parece bien en su mayor parte.

Evidentemente, ya hay respuestas muy informativas aquí, pero le diré lo que hacemos.

No hacemos nada (es decir, no anulamos).

Si necesitamos que equals / hashcode funcione para colecciones, usamos UUID. Usted acaba de crear el UUID en el constructor. Usamos http://wiki.fasterxml.com/JugHome para UUID. UUID es un poco más costoso para la CPU, pero es barato en comparación con la serialización y el acceso a bases de datos.

El enfoque de claves empresariales no nos sirve. We use DB generated ID , temporary transient tempId and override equal()/hashcode() to solve the dilemma. All entities are descendants of Entity. Pros:

  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

Contras:

  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. ¿Qué más?

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:

  • sencillez
  • ensures subclasses provide type identity
  • predicted behavior with proxied classes

Desventajas:

  • Requires each entity to call super()

Notas:

  • 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

Esperamos sus comentarios.

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?

por ejemplo:

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