example - object equals java
El JPA hashCode()/equals() dilema (19)
Ha habido some discussions aquí sobre las entidades JPA y qué hashCode()
/ equals()
debe usar para las clases de entidades JPA. La mayoría (si no todos) de ellos dependen de Hibernate, pero me gustaría discutirlos de forma neutra con JPA (estoy utilizando EclipseLink, por cierto).
Todas las implementaciones posibles tienen sus propias ventajas y desventajas con respecto a:
-
hashCode()
/equals()
conformidad de contrato (inmutabilidad) para operaciones deList
/Set
- Se pueden detectar objetos idénticos (por ejemplo, de diferentes sesiones, proxies dinámicos de estructuras de datos cargadas perezosamente)
- Si las entidades se comportan correctamente en estado separado (o no persistente)
Por lo que puedo ver, hay tres opciones :
- No los anules; confiar en
Object.equals()
yObject.hashCode()
-
hashCode()
/equals()
trabajo - no se pueden identificar objetos idénticos, problemas con proxies dinámicos
- no hay problemas con las entidades separadas
-
- Anularlos, según la clave principal
-
hashCode()
/equals()
están rotos - Identidad correcta (para todas las entidades gestionadas)
- problemas con entidades separadas
-
- Anularlos, en función de la identificación de la empresa (campos de clave no principal; ¿qué pasa con las claves externas?)
-
hashCode()
/equals()
están rotos - Identidad correcta (para todas las entidades gestionadas)
- no hay problemas con las entidades separadas
-
Mis preguntas son:
- ¿Me perdí una opción y / o un punto pro / con?
- ¿Qué opción elegiste y por qué?
ACTUALIZACIÓN 1:
Por " hashCode()
/ equals()
se rompe", me refiero a que las sucesivas invocaciones de hashCode()
pueden devolver valores diferentes, lo que no se rompe (cuando se implementa correctamente) en el sentido de la documentación de la API de Object
, pero causa 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é método será el mejor para una aplicación de la vida real o cómo determinar cuál es el mejor método para mi aplicación. Por lo tanto, mantendré la pregunta abierta y esperaré más discusiones u opiniones.
- Si tiene una clave de negocio , debe usarla para
equals
/hashCode
. - Si no tiene una clave de negocio, no debe dejarla con las implementaciones predeterminadas de
Object
igual y hashCode, ya que no funciona después de lamerge
y la entidad. Puede usar el identificador de entidad como se sugiere en esta publicación . El único problema es que necesitas usar una implementación de
hashCode
que siempre devuelve el mismo valor, como esto:@Entity public class Book implements Identifiable<Long> { @Id @GeneratedValue private Long id; private String title; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book book = (Book) o; return getId() != null && Objects.equals(getId(), book.getId()); } @Override public int hashCode() { return 31; } //Getters and setters omitted for brevity }
Aunque el uso de una clave de negocio (opción 3) es el método más comúnmente recomendado ( wiki de la comunidad de Hibernate , "Persistencia de Java con Hibernate", pág. 398), y esto es lo que usamos principalmente, hay un error de Hibernate que lo rompe por algo que buscamos. Conjuntos: 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 de clave de negocios recomendado sea problemático.
Creo que el meollo de la cuestión es que equals y hashCode deben basarse en un estado inmutable (referencia Odersky et al. ), Y una entidad de Hibernate con clave primaria administrada por Hibernate no tiene tal estado inmutable. La clave principal 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, heredar las implementaciones java.lang.Object basadas en la identidad del objeto, o usar una clave primaria administrada por la aplicación como lo sugiere James Brundege en "No dejes que Hibernate robe tu identidad" (ya referenciada por la respuesta de Stijn Geukens ) y por Lance Arlaus en "Generación de objetos: un mejor enfoque para la integración de hibernación" .
El mayor problema con la opción 1 es que las instancias separadas no pueden compararse con las instancias persistentes que usan .equals (). Pero eso esta bien; El contrato de equals y hashCode lo deja al desarrollador para decidir qué significa la igualdad para cada clase. Así que simplemente deja 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 ese propósito, tal vez boolean sameEntity
o boolean dbEquivalent
o boolean businessEquals
.
Como ya han señalado otras personas más inteligentes que yo, hay una gran cantidad de estrategias por ahí. Sin embargo, parece ser el caso de que la mayoría de los patrones de diseño aplicados intentan abrirse camino hacia el éxito. Limitan el acceso al constructor si no obstaculizan completamente las invocaciones del constructor con constructores especializados y métodos de fábrica. De hecho, siempre es agradable con una API clara. Pero si la única razón es hacer que las modificaciones de los códigos de hash y igual sean compatibles con la aplicación, entonces me pregunto si esas estrategias cumplen con KISS (Keep It Simple Stupid).
Para mí, me gusta anular los iguales y el código hash mediante el examen de la identificación. En estos métodos, requiero que el ID no sea nulo y documente bien este comportamiento. Por lo tanto, se convertirá en el contrato de los desarrolladores para conservar una nueva entidad antes de almacenarlo en otro lugar. Una aplicación que no cumpla con este contrato fallaría en el minuto (con suerte).
Sin embargo, tenga cuidado: si sus entidades se almacenan en diferentes tablas y su proveedor usa una estrategia de autogeneración para la clave principal, entonces obtendrá claves primarias duplicadas en todos los tipos de entidades. En tal caso, también compare los tipos de tiempo de ejecución con una llamada a Object#getClass() que, por supuesto, hará imposible que dos tipos diferentes se consideren iguales. Eso me queda bien en su mayor parte.
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 ver la generación perezosa de 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();
}
}
Lea este muy buen artículo sobre el tema: No permita que Hibernate robe su identidad .
La conclusión del artículo es la siguiente:
La identidad de 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 totalmente de permitir que los objetos existan sin una identificación antes de que se guarden. Podemos resolver estos problemas asumiendo la responsabilidad de asignar identificaciones de objetos fuera de los marcos de mapeo objeto-relacionales, como Hibernate. En su lugar, las ID de objeto 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.
Normalmente tenemos dos identificaciones en nuestras entidades:
- 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).
- Es para nuestras necesidades de aplicación (
equals()
yhashCode()
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, i.e. subclassing it.
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (!(obj instanceof User))
return false;
return getUuid().equals(((User) obj).getUuid());
}
// Getters and setters follow
}
EDIT: 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("[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
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 se verí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 y un definidor predeterminados, pero puede encontrar el enfoque de dos constructores más adecuado para usted.
Personalmente ya utilicé estas tres estrategias en diferentes proyectos. Debo decir que la opción 1 es, en mi opinión, la más práctica en una aplicación de la vida real. La experiencia de romper el hashCode () / equals () lleva a muchos errores locos, ya que siempre terminará en situaciones en las que el resultado de cambios de igualdad después de que una entidad se haya agregado a una colección.
Pero hay otras 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 principal asignada por la aplicación (en el constructor) en lugar de JPA
(+) Los tres criterios están garantizados.
(-) no puede aprovechar las estrategias de generación de ID confiables simples como las secuencias de DB
(-) se complica 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 UUID
(-) puede haber un pequeño riesgo de que se use dos veces el mismo UUID, dependiendo del algoritmo utilizado (puede detectarse mediante un índice único en la base de datos)
Si desea usar equals()/hashCode()
para sus Conjuntos, en el sentido de que la misma entidad solo puede estar allí una vez, entonces solo hay una opción: Opción 2. Eso es una clave primaria para una entidad por definición nunca cambia (si alguien lo actualiza, ya no es la misma entidad)
Debe tomar eso literalmente: ya que su equals()/hashCode()
se basa en la clave principal, no debe usar estos métodos hasta que se establezca la clave principal. Así que no deberías 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 antes de tiempo).
Ahora, en teoría, también es posible lograrlo con la Opción 3, aunque las llamadas "claves de negocios" tienen el inconveniente desagradable de que pueden cambiar: "Todo lo que tendrá que hacer es eliminar las entidades ya insertadas del conjunto ( s), y reinsertarlos. Eso es cierto, pero también significa que, en un sistema distribuido, tendrá que asegurarse de que esto se haga absolutamente en cualquier lugar donde se hayan insertado los datos (y tendrá que asegurarse de que se realiza 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 son de la misma sesión de Hibernate. La documentación de Hibernate lo deja muy claro en el capítulo 13.1.3. Teniendo en cuenta la identidad del objeto :
Dentro de una sesión, la aplicación puede usar con seguridad == para comparar objetos.
Sin embargo, una aplicación que utiliza == 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 podrían tener la misma identidad de base de datos (es decir, representan la misma fila). Sin embargo, la identidad de JVM no está garantizada, por definición, para instancias en un estado separado. El desarrollador debe anular los métodos equals () y hashCode () en clases persistentes e implementar su propia noción de igualdad de objetos.
Continúa argumentando a favor de la Opción 3:
Hay una advertencia: nunca use el identificador de base de datos para implementar la igualdad. Utilice una clave de negocio que sea una combinación de atributos únicos, generalmente inmutables. El identificador de la base de datos cambiará si un objeto transitorio se hace persistente. Si la instancia transitoria (generalmente junto con las instancias separadas) se mantiene en un Conjunto, el cambio del hashcode rompe el contrato del Conjunto.
Esto es verdad, si usted
- no se puede asignar la identificación antes (por ejemplo, mediante el uso de UUID)
- y sin embargo, absolutamente desea poner sus objetos en conjuntos mientras están en estado transitorio.
De lo contrario, eres libre de elegir la Opción 2.
Luego menciona la necesidad de una estabilidad relativa:
Los atributos para las claves de negocios no tienen que ser tan estables como las claves primarias de la base de datos; solo tiene que 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 "siempre que los objetos estén en el mismo Conjunto"? Puedo imaginar algunos casos especiales (como usar conjuntos solo para una conversación y luego desecharlos), pero cuestionaría la posibilidad general de esto.
Version corta:
- La opción 1 solo se puede utilizar con objetos dentro de 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 asigne la PK).
- Si puede garantizar una estabilidad relativa, puede usar la Opción 3. Pero tenga cuidado con esto.
Siempre sobrescribo equals / hashcode y lo implemento en función del ID de empresa. Parece la solución más razonable para mí. Vea el siguiente link .
Para resumir todo esto, aquí hay una lista de lo que funcionará o no funcionará con las diferentes formas de manejar equals / hashCode:
EDITAR :
Para explicar por qué esto funciona para mí:
- Normalmente no uso la colección basada en hash (HashMap / HashSet) en mi aplicación JPA. Si debo hacerlo, prefiero crear la solución UniqueList.
- Creo que cambiar la identificación empresarial en tiempo de ejecución no es una buena práctica para ninguna aplicación de base de datos. En los casos excepcionales en los que no hay otra solución, haría un tratamiento especial como eliminar el elemento y volver a colocarlo en la colección basada en hash.
- Para mi modelo, establezco el ID de negocio en el constructor y no proporciono configuradores para él. Dejo la implementación de JPA para cambiar el campo en lugar de la propiedad.
- La solución UUID parece ser una exageración. ¿Por qué UUID si tienes identificación de negocio natural? Después de todo, establecería la unicidad del ID de empresa en la base de datos. ¿Por qué tener TRES índices para cada tabla en la base de datos entonces?
Considere el siguiente enfoque basado en el identificador de tipo predefinido y el ID.
Los supuestos específicos para JPA:
- Las entidades del mismo "tipo" y la misma ID no nula se consideran iguales
- las entidades no persistentes (suponiendo que no hay ID) nunca son iguales a otras entidades
La entidad abstracta:
@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {
@Id @GeneratedValue
private K id;
@Transient
private final String kind;
public AbstractPersistable(final String kind) {
this.kind = requireNonNull(kind, "Entity kind cannot be null");
}
@Override
public final boolean equals(final Object obj) {
if (this == obj) return true;
if (!(obj instanceof AbstractPersistable)) return false;
final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
return null != this.id
&& Objects.equals(this.id, that.id)
&& Objects.equals(this.kind, that.kind);
}
@Override
public final int hashCode() {
return Objects.hash(kind, id);
}
public K getId() {
return id;
}
protected void setId(final K id) {
this.id = id;
}
}
Ejemplo de entidad concreta:
static class Foo extends AbstractPersistable<Long> {
public Foo() {
super("Foo");
}
}
Ejemplo de prueba:
@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());
}
Principales ventajas aquí:
- sencillez
- asegura que las subclases proporcionan la identidad de tipo
- comportamiento predicho con clases de proxy
Desventajas:
- Requiere que cada entidad llame
super()
Notas:
- Necesita atención al usar la herencia. Por ejemplo, la igualdad de
class A
yclass B extends A
puede depender de detalles concretos de la aplicación. - Lo ideal es utilizar una clave de negocio como la ID
Esperamos sus comentarios.
En mi opinión, tienes 3 opciones para implementar equals / hashCode
- Usar una identidad generada por la aplicación, es decir, un UUID
- Implementarlo en base a una clave de negocio.
- Implementarlo basado en la clave primaria.
Usar una identidad generada por la aplicación es el enfoque más fácil, pero viene con algunas desventajas
- Las uniones son más lentas cuando se usa como PK porque 128 Bit es simplemente más grande que 32 o 64 Bit
- "Depurar es más difícil" porque verificar con tus propios ojos si algunos datos son correctos es bastante difícil
Si puede trabajar con estos inconvenientes , solo use este enfoque.
Para superar el problema de la unión, uno podría usar el UUID como clave natural y un valor de secuencia como clave principal, pero es posible que aún se encuentre con los problemas de implementación equals / hashCode en entidades secundarias compositivas que tienen identificadores integrados, ya que querrá unirse en la clave principal. El uso de la clave natural en el ID de las entidades secundarias y la clave principal para referirse al padre es un buen compromiso.
@ManagedBean
public class MyCarFacade {
public Car createCar(){
Car car = new Car();
em.persist(car);
return car;
}
}
OMI, este es el enfoque más limpio, ya que evitará todos los inconvenientes y, al mismo tiempo, le proporcionará un valor (el UUID) que puede compartir con sistemas externos sin exponer los elementos internos del sistema.
Implementarlo basado en una clave de negocios si puede esperar que de un usuario sea una buena idea, pero que también tenga algunas desventajas
La mayoría de las veces, esta clave de negocio será algún tipo de código que proporcione el usuario y, con menos frecuencia, un compuesto de múltiples atributos.
- Las uniones son más lentas porque las uniones basadas en texto de longitud variable son simplemente lentas. Algunos DBMS pueden incluso tener problemas para crear un índice si la clave excede una cierta longitud.
- En mi experiencia, las claves de negocios tienden a cambiar, lo que requerirá actualizaciones en cascada de los objetos que se refieren a él. Esto es imposible si los sistemas externos se refieren a ello.
En mi opinión, no debe implementar o trabajar con una clave comercial exclusivamente. Es un buen complemento, es decir, los usuarios pueden buscar rápidamente por esa clave de negocio, pero el sistema no debe confiar en él para operar.
Implementarlo basado en la clave principal tiene sus problemas, pero tal vez no sea tan importante
Si necesita exponer las identificaciones a un sistema externo, use el enfoque UUID que sugerí. Si no lo hace, aún podría utilizar el enfoque UUID pero no tiene que hacerlo. El problema de usar un id de DBMS generado en equals / hashCode se deriva del hecho de que el objeto podría haberse agregado a las colecciones basadas en hash antes de asignar el id.
La forma obvia de evitar esto es simplemente no agregar el objeto a las colecciones basadas en hash antes de asignar el ID. Entiendo que esto no siempre es posible porque es posible que desee la deduplicación antes de asignar la ID ya. Para poder seguir utilizando las colecciones basadas en hash, simplemente debe reconstruir las colecciones después de asignar el ID.
Podrías hacer algo como esto:
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;
}
No he probado el enfoque exacto, por lo que no estoy seguro de cómo funciona el cambio de colecciones en eventos anteriores y posteriores a la persistencia, pero la idea es:
- Eliminar temporalmente el objeto de las colecciones basadas en hash
- Persistirlo
- Vuelva a agregar el objeto a las colecciones basadas en hash
Otra forma de resolver esto es simplemente reconstruir todos sus modelos basados en hash después de una actualización / persistencia.
Al final, depende de ti. Personalmente uso el enfoque basado en secuencias la mayor parte del tiempo y solo uso el enfoque UUID si necesito exponer un identificador a sistemas externos.
Obviamente ya hay respuestas muy informativas aquí, pero les diré lo que hacemos.
No hacemos nada (es decir, no anular).
Si necesitamos iguales / hashcode para trabajar con las colecciones, usamos UUID. Solo creas el UUID en el constructor. Usamos http://wiki.fasterxml.com/JugHome para UUID. UUID es un CPU un poco más caro, pero es barato en comparación con la serialización y el acceso a base de datos.
Si UUID es la respuesta para muchas personas, ¿por qué no solo usamos métodos de fábrica de la capa empresarial para crear las entidades y asignar la clave principal en el momento de la creación?
por ejemplo:
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;
}
de esta manera obtendríamos una clave primaria predeterminada para la entidad del proveedor de persistencia, y nuestras funciones hashCode () y equals () podrían basarse en eso.
También podríamos declarar protegidos a los constructores del Coche y luego usar la reflexión en nuestro método comercial para acceder a ellos. De esta manera, los desarrolladores no tienen la intención de crear una instancia de Car con el nuevo, sino a través del método de fábrica.
¿Qué tal eso?
A continuación se muestra una solución simple (y probada) para Scala.
Tenga en cuenta que esta solución no encaja en ninguna de las 3 categorías dadas en la pregunta.
Todas mis Entidades son subclases de la UUIDEntidad, así que sigo el principio de no repetirse (DRY).
Si es necesario, la generación de UUID se puede hacer más precisa (utilizando más números pseudoaleatorios).
Código Scala:
import javax.persistence._ import scala.util.Random @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) abstract class UUIDEntity { @Id @GeneratedValue(strategy = GenerationType.TABLE) var id:java.lang.Long=null var uuid:java.lang.Long=Random.nextLong() override def equals(o:Any):Boolean= o match{ case o : UUIDEntity => o.uuid==uuid case _ => false } override def hashCode() = uuid.hashCode() }
El enfoque de las claves de negocios no nos conviene. Utilizamos DB generó ID , transitoria temporal IDTEMP y anula igual () / hashCode () para resolver el dilema. Todas las entidades son descendientes de la entidad. Pros:
- No hay campos extra en DB
- No hay codificación extra en las entidades descendientes, un enfoque para todos
- Sin problemas de rendimiento (como con UUID), generación de ID de DB
- No hay problema con los Hashmaps (no es necesario tener en cuenta el uso de iguales, etc.)
- El código de hash de la nueva entidad no cambia a tiempo incluso después de persistir
Contras:
- Puede haber problemas con la serialización y deserialización de entidades no persistentes
- El código de hash de la entidad guardada puede cambiar después de volver a cargar desde DB
- Objetos no persistentes considerados siempre diferentes (tal vez esto es correcto?)
- ¿Qué más?
Mira nuestro código:
@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();
}
}
En la práctica, parece que la Opción 2 (Clave principal) se usa con más frecuencia. Las claves de negocio naturales e INMUTABLES son pocas veces, la creación y el soporte de claves sintéticas son demasiado pesadas para resolver situaciones, que probablemente nunca ocurran. Eche un vistazo a la implementación Abstract-Persistable de spring-data-jpa (lo único: para el uso de la implementación de HibernateHibernate.getClass
).
public boolean equals(Object obj) { if (null == obj) { return false; } if (this == obj) { return true; } if (!getClass().equals(ClassUtils.getUserClass(obj))) { return false; } AbstractPersistable<?> that = (AbstractPersistable<?>) obj; return null == this.getId() ? false : this.getId().equals(that.getId()); } @Override public int hashCode() { int hashCode = 17; hashCode += null == getId() ? 0 : getId().hashCode() * 31; return hashCode; }
Solo consciente de manipular objetos nuevos en HashSet / HashMap. En el opuesto, la Opción 1 (permanecer en la Object
implementación) se rompe justo después merge
, esa es una situación muy común.
Si no tiene una clave comercial y tiene una REAL necesidad de manipular una nueva entidad en la estructura hash, anule hashCode
a constante, como se indicó a continuación Vlad Mihalcea.
Este es un problema común en todos los sistemas de TI que usan Java y JPA. El punto de dolor se extiende más allá de la implementación de equals () y hashCode (), afecta a cómo una organización se refiere a una entidad y cómo sus clientes se refieren a la misma entidad. He visto suficiente dolor por no tener una clave de negocios hasta el punto de que escribí mi propio blog para expresar mi opinión.
En resumen: use una ID secuencial, corta y legible por humanos, con prefijos significativos como clave de negocio que se genere sin ninguna dependencia de ningún almacenamiento que no sea RAM. El Snowflake de Snowflake de Twitter es un muy buen ejemplo.
Intenté responder esta pregunta por mí mismo y nunca estuve del todo contento con las soluciones encontradas hasta que leí esta publicación y, en especial, DREW. Me gustó la forma en que el perezoso creó UUID y lo almacenó de manera óptima.
Pero quería agregar aún más flexibilidad, es decir, crear UUID de forma perezosa SOLO cuando se accede a hashCode () / equals () antes de la primera persistencia de la entidad con las ventajas de cada solución:
- equals () significa "objeto se refiere a la misma entidad lógica"
- Usar la ID de la base de datos tanto como sea posible porque ¿por qué haría el trabajo dos veces (problema de rendimiento)
- evitar problemas al acceder a hashCode () / equals () en una entidad que aún no se ha conservado y mantener el mismo comportamiento después de que se haya persistido
Realmente apreciaría los comentarios sobre mi solución mixta a continuación
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() }
Siempre he usado la opción 1 en el pasado porque estaba al tanto de estas discusiones y pensé que era mejor no hacer nada hasta que supiera qué hacer. Esos sistemas todavía se están ejecutando con éxito.
Sin embargo, la próxima vez puedo probar la opción 2: usar el ID generado por la base de datos.
Hashcode y equals lanzarán la excepción IllegalStateException si el ID no está establecido.
Esto evitará que aparezcan inesperadamente errores sutiles relacionados con entidades no guardadas.
¿Qué piensa la gente de este enfoque?