El dilema JPA hashCode () / equals ()


311

Ha habido algunas discusiones aquí acerca de las entidades JPA y qué hashCode()/ equals()implementación debería usarse para las clases de entidades JPA. La mayoría (si no todos) de ellos dependen de Hibernate, pero me gustaría discutirlos de manera neutral en la implementación de JPA (por cierto, estoy usando EclipseLink).

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

  • hashCode()/ equals()conformidad del contrato (inmutabilidad) para List/ Setoperaciones
  • Si se pueden detectar objetos idénticos (por ejemplo, de diferentes sesiones, proxys dinámicos de estructuras de datos con carga lenta)
  • Si las entidades se comportan correctamente en estado separado (o no persistente)

Hasta donde puedo ver, hay tres opciones :

  1. No los anule; confiar Object.equals()yObject.hashCode()
    • hashCode()/ equals()trabajo
    • no puede identificar objetos idénticos, problemas con proxys dinámicos
    • sin problemas con entidades separadas
  2. Anularlos, según la clave primaria
    • hashCode()/ equals()están rotos
    • identidad correcta (para todas las entidades gestionadas)
    • problemas con entidades separadas
  3. Anularlos, en función del ID de negocio (campos de clave no primarios; ¿qué pasa con las claves externas?)
    • hashCode()/ equals()están rotos
    • identidad correcta (para todas las entidades gestionadas)
    • sin problemas con entidades separadas

Mis preguntas son:

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



ACTUALIZACIÓN 1:

Por " hashCode()/ equals()están rotos", quiero decir que las sucesivas hashCode()invocaciones pueden devolver valores diferentes, que es (cuando se implementa correctamente) no rota en el sentido de la Objectdocumentación de la API, pero que causa problemas al intentar recuperar una entidad cambió de una Map, Setu otra basado en hash Collection. En consecuencia, las implementaciones 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.
Desafortunadamente, 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, mantendré la pregunta abierta y espero más discusiones y / u opiniones.


44
No entiendo qué quieres decir con "hashCode () / equals () broken"
nanda

44
Entonces no estarían "rotos" en ese sentido, ya que en las opciones 2 y 3 se implementaría tanto equals () como hashCode () utilizando la misma estrategia.
mate b

11
Eso no es cierto para la opción 3. hashCode () y equals () deberían estar usando los mismos criterios, por lo tanto, si uno de sus campos cambia, sí, el método hashcode () devolverá un valor diferente para la misma instancia que lo hizo anteriormente, pero también será igual a (). Has omitido la segunda parte de la oración del hashcode () javadoc: cada vez que se invoca en el mismo objeto más de una vez durante la ejecución de una aplicación Java, el método hashCode debe devolver constantemente el mismo entero, siempre que no haya información utilizado en comparaciones iguales en el objeto se modifica .
mate b

1
En realidad, esa parte de la oración significa lo contrario: llamar hashcode()a la misma instancia de objeto debería devolver el mismo valor, a menos que equals()cambie cualquier campo utilizado en la implementación. En otras palabras, si tiene tres campos en su clase y su equals()método usa solo dos de ellos para determinar la igualdad de instancias, entonces puede esperar que el hashcode()valor de retorno cambie si cambia uno de los valores de ese campo, lo cual tiene sentido cuando considera que esta instancia de objeto ya no es "igual" al valor que representaba la instancia anterior.
mate b

2
"problemas al intentar recuperar una entidad modificada de un mapa, conjunto u otras colecciones basadas en hash" ... esto debería ser "problemas al intentar recuperar una entidad modificada de un HashMap, HashSet u otras colecciones basadas en hash"
nanda

Respuestas:


122

Lea este bonito artículo sobre el tema: No deje que Hibernate le robe su identidad .

La conclusión del artículo es así:

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 surgen 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 ID de objetos lejos de los marcos de mapeo relacional de objetos como Hibernate. En cambio, se pueden asignar ID de objeto tan pronto como se instancia el objeto. Esto hace que la identidad del objeto sea simple y libre de errores, y reduce la cantidad de código necesaria en el modelo de dominio.


21
No, ese no es un buen artículo. ¡Es un excelente artículo sobre el tema, y ​​se debe leer para todos los programadores de JPA! +1!
Tom Anderson el

2
Sí, estoy usando la misma solución. No permitir que la base de datos genere la ID también tiene otras ventajas, como poder crear un objeto y crear otros objetos que lo hagan referencia antes de persistir. Esto puede eliminar la latencia y los múltiples ciclos de solicitud / respuesta en las aplicaciones cliente-servidor. Si necesita inspiración para tal solución, consulte mis proyectos: suid.js y suid-server-java . Básicamente suid.jsrecupera bloques de identificación de los suid-server-javaque luego puede obtener y usar el lado del cliente.
Stijn de Witt

2
Esto es simplemente una locura. Soy nuevo en hibernar el funcionamiento bajo el capó, estaba escribiendo pruebas unitarias y descubrí que no puedo eliminar un objeto de un conjunto después de modificarlo, concluí que se debe al cambio del código hash, pero no pude entender cómo resolver. ¡El artículo es simplemente hermoso!
XMight

Es un gran articulo. Sin embargo, para las personas que ven el enlace por primera vez, sugeriría que podría ser una exageración para la mayoría de las aplicaciones. Las otras 3 opciones enumeradas en esta página deberían resolver más o menos el problema de varias maneras.
HopeKing

1
¿Hibernate / JPA utiliza el método igual y hashcode de una entidad para verificar si el registro ya existe en la base de datos?
Tushar Banne

64

Siempre anulo equals / hashcode y lo implemento en función de la identificación comercial. Parece la solución más razonable para mí. Ver 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: ingrese la descripción de la imagen aquí

EDITAR :

Para explicar por qué esto funciona para mí:

  1. Normalmente no uso la colección basada en hash (HashMap / HashSet) en mi aplicación JPA. Si debo hacerlo, prefiero crear una solución UniqueList.
  2. Creo que cambiar la identificación comercial en tiempo de ejecución no es una práctica recomendada para ninguna 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 colocarlo en la colección basada en hash.
  3. Para mi modelo, configuré la identificación comercial en el constructor y no proporciono configuradores para ello. Dejo que la implementación de JPA cambie el campo en lugar de la propiedad.
  4. La solución UUID parece ser exagerada. ¿Por qué UUID si tiene una identificación comercial natural? Después de todo, establecería la unicidad de la identificación comercial en la base de datos. ¿Por qué entonces tener TRES índices para cada tabla en la base de datos?

1
Pero a esta tabla le falta una quinta línea "funciona con Lista / Conjuntos" (si piensa en eliminar una entidad que forma parte de un Conjunto de una asignación de OneToMany) a la que se respondería "No" en las dos últimas opciones porque su código hash ( ) cambios que violen su contrato.
MRalwasser

Ver el comentario sobre la pregunta. Parece que malinterpretas el contrato igual / hashcode
nanda

1
@MRalwasser: Creo que te refieres a lo correcto, simplemente no es el contrato equals / hashCode () lo que se viola. Pero un mutable equals / hashCode crea problemas con el contrato Set .
Chris Lercher

3
@MRalwasser: el código hash solo puede cambiar si la ID de la empresa cambia, y el punto es que la ID de la empresa no cambia. Por lo tanto, el código hash no cambia, y esto funciona perfectamente con colecciones hash.
Tom Anderson el

1
¿Qué pasa si no tiene una clave comercial natural? Por ejemplo, en el caso de un punto bidimensional, Punto (X, Y), en una aplicación de dibujo gráfico? ¿Cómo almacenarías ese punto como una Entidad?
jhegedus

35

Generalmente tenemos dos ID en nuestras entidades:

  1. Es solo para la capa de persistencia (para que el proveedor de persistencia y la base de datos puedan descubrir relaciones entre objetos)
  2. Es para nuestras necesidades de aplicación ( 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, 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
}

EDITAR: para aclarar mi punto con respecto a las llamadas al setUuid()método. 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 la salida 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 predeterminado y un setter, pero puede encontrar el enfoque de dos constructores más adecuado para usted.


2
Creo que esta es una solución correcta y buena. También puede tener una pequeña ventaja de rendimiento, porque los enteros generalmente funcionan mejor en los índices de base de datos que los uuids. Pero aparte de eso, ¿probablemente podría eliminar la propiedad de identificación de número entero actual y reemplazarla con el uuid (aplicación asignada)?
Chris Lercher

44
¿En qué se diferencia esto de usar los métodos hashCode/ predeterminados equalspara la igualdad de JVM y idpara la igualdad de persistencia? Esto no tiene sentido para mí en absoluto.
Behrang Saeedzadeh

2
Funciona en casos cuando tiene varios objetos de entidad apuntando a la misma fila en una base de datos. Object's equals()volvería falseen este caso. UUID basados en equals()rendimientos true.
Andrew Андрей Листочкин

44
-1 - No veo ninguna razón para tener dos ID, y por lo tanto dos tipos de identidad. Esto parece completamente inútil y potencialmente dañino para mí.
Tom Anderson el

1
Perdón por criticar su solución sin señalar una que preferiría. En resumen, le daría a los objetos un solo campo de ID, implementaría igual y hashCode basado en él, y generaría su valor en la creación de objetos, en lugar de guardarlos en la base de datos. De esa manera, todas las formas del objeto funcionan de la misma manera: no persistente, persistente y desapegada. Los servidores proxy de Hibernate (o similares) también deberían funcionar correctamente, y creo que ni siquiera necesitarían hidratarse para manejar llamadas iguales y hashCode.
Tom Anderson el

31

Personalmente, ya utilicé 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. En mi experiencia, romper la conformidad hashCode () / equals () conduce a muchos errores locos, ya que siempre terminará 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 otras opciones (también con sus ventajas y desventajas):


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

(-) complica el manejo si debe cambiar uno de ellos


b) hashCode / equals basado en una clave primaria 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 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

(-) gastos generales de generación de UUID

(-) puede ser un pequeño riesgo de que se use dos veces el mismo UUID, dependiendo del algoritmo utilizado (puede ser detectado por un índice único en DB)


Soy fanático de la Opción 1 y del Enfoque C también. No haga nada hasta que lo necesite absolutamente, es el enfoque más ágil.
Adam Gent

2
+1 para la opción (b). En mi humilde opinión, si una entidad tiene una identificación comercial natural, entonces esa también debería ser su clave principal de la base de datos. Eso es simple, directo, buen diseño de base de datos. Si no tiene esa ID, se necesita una clave sustituta. Si configura eso en la creación de objetos, entonces todo lo demás es simple. Es cuando las personas no usan una clave natural, y no generan una clave sustituta temprano que se meten en problemas. En cuanto a la complejidad en la implementación, sí, hay algunas. Pero realmente no mucho, y se puede hacer de una manera muy genérica que lo resuelva una vez para todas las entidades.
Tom Anderson el

También prefiero la opción 1, pero entonces cómo escribir una prueba unitaria para afirmar la igualdad total es un gran problema, porque tenemos que implementar el método de igualdad para Collection.
OOD Waterball

Solo no lo hagas. Ver No te
metas

29

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 porque una clave principal para una entidad por definición nunca cambia (si alguien realmente actualiza ya no es la misma entidad)

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

Ahora, teóricamente, 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 vuelva a insertarlos ". Eso es cierto, pero también significa que, en un sistema distribuido, tendrá que asegurarse de que esto se haga absolutamente en todas partes donde se hayan insertado los datos (y deberá asegurarse de que la actualización se realice , antes de que ocurran otras cosas). Necesitará un mecanismo de actualización sofisticado, especialmente si actualmente no se puede acceder a algunos sistemas remotos ...

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. Considerando la identidad del objeto :

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

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 podrían tener la misma identidad de base de datos (es decir, representan la misma fila). Sin embargo, la identidad de JVM, por definición, no está garantizada para 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 hace persistente. Si la instancia transitoria (generalmente junto con las instancias separadas) se mantiene en un Conjunto, al cambiar el código hash se rompe el contrato del Conjunto.

Esto es cierto, si usted

  • no puede asignar la identificación antes (por ejemplo, usando UUID)
  • y, sin embargo, desea colocar sus objetos en conjuntos mientras están en estado transitorio.

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

Luego menciona la necesidad de una relativa estabilidad:

Los atributos para las claves comerciales 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 "mientras los objetos estén en el mismo Conjunto". 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 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 el PK).
  • Si puede garantizar una relativa estabilidad, puede usar la Opción 3. Pero tenga cuidado con esto.

Su suposición de que la clave primaria nunca cambia es falsa. Por ejemplo, Hibernate solo asigna la clave primaria cuando se guarda la sesión. Por lo tanto, si utiliza la clave primaria como hashCode, el resultado de hashCode () antes de guardar el objeto por primera vez y después de guardarlo por primera vez será diferente. Peor aún, antes de guardar la sesión, dos objetos recién creados tendrán el mismo código hash y pueden sobrescribirse cuando se agreguen a las colecciones. Es posible que tenga que forzar un guardado / descarga inmediatamente en la creación de objetos para usar ese enfoque.
William Billingsley

2
@William: la clave principal de una entidad no cambia. La propiedad id del objeto asignado puede cambiar. Esto ocurre, como usted explicó, especialmente cuando un objeto transitorio se hace persistente . Lea atentamente la parte de mi respuesta, donde dije acerca de los métodos equals / hashCode: "no debe usar estos métodos hasta que se establezca la clave principal".
Chris Lercher

Totalmente de acuerdo. Con la opción 2, también puede factorizar igual / hashcode en una superclase y hacer que todas sus entidades lo reutilicen.
Theo

+1 Soy nuevo en JPA, pero algunos de los comentarios y respuestas aquí implican que las personas no entienden el significado del término "clave principal".
Raedwald

16
  1. Si tiene una clave comercial , debe usarla para equals/ hashCode.
  2. Si no tiene una clave comercial, no debe dejarla con las Objectimplementaciones predeterminadas de igual y hashCode porque eso no funciona después de usted mergey la entidad.
  3. Puede usar el identificador de entidad como se sugiere en esta publicación . El único inconveniente es que necesita usar una hashCodeimplementación que siempre devuelva el mismo valor, como este:

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

¿Qué es mejor: (1) onjava.com/pub/a/onjava/2006/09/13/… o (2) vladmihalcea.com/… ? La solución (2) es más fácil que (1). Entonces, ¿por qué debería usar (1)? ¿Son los efectos de ambos iguales? ¿Ambos garantizan la misma solución?
nimo23

Y con su solución: "el valor de hashCode no cambia" entre las mismas instancias. Esto tiene el mismo comportamiento que si fuera el "mismo" uuid (de la solución (1)) que se compara. Estoy en lo cierto?
nimo23

1
¿Y almacenar el UUID en la base de datos y aumentar la huella del registro y en el grupo de búferes? Creo que esto puede conducir a más problemas de rendimiento a largo plazo que el exclusivo hashCode. En cuanto a la otra solución, puede verificarla para ver si proporciona coherencia en todas las transiciones de estado de entidad. Puede encontrar la prueba que verifica eso en GitHub .
Vlad Mihalcea

1
Si tiene una clave comercial inmutable, el hashCode puede usarla y se beneficiará de múltiples cubos, por lo que vale la pena usarla si tiene una. De lo contrario, solo use el identificador de entidad como se explica en mi artículo.
Vlad Mihalcea

1
Me alegra que te guste. Tengo cientos de artículos más sobre JPA e Hibernate.
Vlad Mihalcea

10

Aunque el uso más común de una clave empresarial (opción 3) es el enfoque más recomendado ( wiki de la comunidad de Hibernate , "Java Persistence with Hibernate" p. 398), y esto es lo que usamos principalmente, hay un error de Hibernate que lo rompe para los ansiosos 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 problemático el enfoque de clave empresarial recomendado.

Creo que el meollo del asunto es que igual y hashCode deberían basarse en un estado inmutable (referencia Odersky et al. ), Y una entidad de Hibernate con clave primaria administrada por Hibernate no tiene ese estado inmutable. Hibernate modifica la clave principal cuando un objeto transitorio se vuelve persistente. Hibernate también modifica la clave empresarial cuando hidrata un objeto en el proceso de inicialización.

Eso deja solo la opción 1, heredar las implementaciones de 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 deje que Hibernate le robe su 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 Hibernate" .

El mayor problema con la opción 1 es que las instancias separadas no se pueden comparar con instancias persistentes usando .equals (). Pero eso esta bien; el contrato de equals y hashCode le deja al desarrollador decidir qué significa igualdad para cada clase. Entonces, solo 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 ese propósito, tal vez boolean sameEntityo boolean dbEquivalento boolean businessEquals.


5

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

}

Bueno, en realidad si anula equals () / hashCode (), debe generar UUID para cada entidad de todos modos (supongo que desea persistir cada entidad que cree en su código). Lo hace solo una vez, antes de almacenarlo en una base de datos por primera vez. Después de eso, el UUID es cargado por el Proveedor de persistencia. Por lo tanto, no veo el punto de hacerlo perezosamente.
Andrew Андрей Листочкин

Elegí su respuesta porque realmente me gustan sus otras ideas: almacenar UUID como un par de números en la base de datos y no emitir a un tipo particular dentro del método equals (), ¡ese es realmente genial! Definitivamente usaré estos dos trucos en el futuro.
Andrew Андрей Листочкин

1
Gracias por el voto positivo. La razón para inicializar perezosamente el UUID fue en nuestra aplicación, creamos muchas Entidades que nunca se ponen en un HashMap o persisten. Entonces vimos una caída del rendimiento de 100x cuando estábamos creando el objeto (100,000 de ellos). Entonces solo iniciamos el UUID si es necesario. Solo desearía que hubiera un buen soporte en MySql para números de 128 bits, por lo que también podríamos usar el UUID para id y no preocuparnos por el auto_increment.
Dibujó

Oh ya veo. En mi caso, ni siquiera declaramos el campo UUID si la entidad correspondiente no se va a colocar en colecciones. El inconveniente es que a veces tenemos que agregarlo porque más tarde resulta que realmente necesitamos ponerlos en colecciones. Esto sucede a veces durante el desarrollo, pero afortunadamente nunca nos sucedió después de la implementación inicial a un cliente, por lo que no fue un gran problema. Si eso sucediera después de que el sistema se activara, necesitaríamos una migración db. Los UUID perezosos son muy útiles en tales situaciones.
Andrew Андрей Листочкин

Quizás también debería probar el generador de UUID más rápido que Adam sugirió en su respuesta si el rendimiento es un problema crítico en su situación.
Andrew Андрей Листочкин

3

Como otras personas más inteligentes que yo ya han señalado, 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 del constructor si no dificultan las invocaciones del constructor completamente 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 anulaciones de código igual y hash sean compatibles con la aplicación, entonces me pregunto si esas estrategias cumplen con KISS (Keep It Simple Stupid).

Para mí, me gusta anular equals y hashcode al examinar la identificación. En estos métodos, requiero 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 cumple con este contrato fallará en un minuto (con suerte).

Sin embargo, una advertencia: si sus entidades se almacenan en tablas diferentes y su proveedor utiliza una estrategia de generación automática para la clave primaria, 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 Object # getClass () que, por supuesto, hará imposible que dos tipos diferentes se consideren iguales. Eso me queda bien en su mayor parte.


Incluso con una DB que faltan secuencias (como Mysql), es posible simularlas (por ejemplo, table hibernate_sequence). Por lo tanto, siempre puede obtener una ID única en todas las tablas. +++ Pero no lo necesitas. Llamar Object#getClass() es malo debido a los representantes de H. Llamar Hibernate.getClass(o)ayuda, pero el problema de la igualdad de las entidades de diferentes tipos persiste. Hay una solución con canEqual , un poco complicada, pero utilizable. De acuerdo en que generalmente no es necesario. +++ Lanzar en eq / hc en ID nulo viola el contrato, pero es muy pragmático.
maaartinus

2

Obviamente ya hay respuestas muy informativas aquí, pero te diré lo que hacemos.

No hacemos nada (es decir, no anulamos).

Si necesitamos equals / hashcode para trabajar en colecciones, usamos UUID. Simplemente crea el UUID en el constructor. Usamos http://wiki.fasterxml.com/JugHome para UUID. UUID es un poco más costoso en cuanto a CPU pero es barato en comparación con la serialización y el acceso db.


1

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 lo correcto. 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 de la base de datos.

Hashcode y equals arrojarán IllegalStateException si el id no está configurado.

Esto evitará que aparezcan inesperadamente errores sutiles que involucren entidades no guardadas.

¿Qué piensa la gente de este enfoque?


1

El enfoque de las claves comerciales no nos conviene. Utilizamos ID generada por DB , tempId transitorio temporal y anulación de equal () / hashcode () para resolver el dilema. Todas las entidades son descendientes de la entidad. Pros:

  1. No hay campos adicionales en DB
  2. Sin codificación adicional en entidades descendientes, un enfoque para todos
  3. Sin problemas de rendimiento (como con UUID), generación de Id. De DB
  4. No hay problema con Hashmaps (no es necesario tener en cuenta el uso de igualdad y etc.)
  5. El código hash de la nueva entidad no cambia a tiempo incluso después de persistir

Contras:

  1. Puede haber problemas con la serialización y deserialización de entidades no persistentes
  2. El código hash de la entidad guardada puede cambiar después de recargar desde DB
  3. Objetos no persistentes considerados siempre diferentes (¿tal vez esto es correcto?)
  4. ¿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();
    }
}

1

Considere el siguiente enfoque basado en el identificador de tipo predefinido y la 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
  • garantiza que las subclases proporcionen identidad de tipo
  • comportamiento predicho con clases proxy

Desventajas

  • Requiere que cada entidad llame super()

Notas:

  • Necesita atención cuando se usa la herencia. Por ejemplo, la igualdad de instancia class Ay class B extends Apuede depender de detalles concretos de la aplicación.
  • Idealmente, use una clave comercial como ID

Esperamos sus comentarios.


0

Este es un problema común en todos los sistemas de TI que utilizan Java y JPA. El punto crítico se extiende más allá de la implementación de equals () y hashCode (), afecta 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 comercial hasta el punto de que escribí mi propio blog para expresar mi opinión.

En resumen: utilice una ID secuencial, legible para humanos, con prefijos significativos como clave comercial que se genera sin depender de ningún almacenamiento que no sea RAM. El copo de nieve de Twitter es un muy buen ejemplo.


0

En mi opinión, tiene 3 opciones para implementar equals / hashCode

  • Utilice una identidad generada por la aplicación, es decir, un UUID
  • Impleméntelo en función de una clave comercial
  • Impleméntelo según la clave primaria

El uso de una identidad generada por la aplicación es el enfoque más fácil, pero tiene 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
  • "La depuración es más difícil" porque verificar con sus propios ojos si algunos datos son correctos es bastante difícil

Si puede trabajar con estos inconvenientes , simplemente use este enfoque.

Para superar el problema de la unión, uno podría estar usando el UUID como clave natural y un valor de secuencia como clave principal, pero aún podría encontrarse con los problemas de implementación de igual / hashCode en entidades secundarias de composición que tienen identificadores integrados ya que querrá unirse en la clave primaria. El uso de la clave natural en el ID de entidades secundarias y la clave primaria para referirse al padre es un buen compromiso.

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

En mi opinión, este es el enfoque más limpio, ya que evitará todas las desventajas y al mismo tiempo le proporcionará un valor (el UUID) que puede compartir con sistemas externos sin exponer las partes internas del sistema.

Impleméntelo según una clave comercial si puede esperar que un usuario sea una buena idea, pero también tiene algunas desventajas

La mayoría de las veces, esta clave comercial será algún tipo de código que el usuario proporcione y, con menos frecuencia, un compuesto de múltiples atributos.

  • Las uniones son más lentas porque la unión basada en texto de longitud variable es simplemente lenta. Algunos DBMS pueden incluso tener problemas para crear un índice si la clave excede cierta longitud.
  • En mi experiencia, las claves de negocio 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 él

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 comercial, pero el sistema no debe depender de él para operar.

Implementarlo en función de la clave principal tiene sus problemas, pero tal vez no sea tan importante

Si necesita exponer identificadores a un sistema externo, use el enfoque UUID que sugerí. Si no lo hace, aún podría usar el enfoque UUID pero no tiene que hacerlo. El problema de usar un ID generado por DBMS en equals / hashCode se debe al hecho de que el objeto podría haberse agregado a colecciones basadas en hash antes de asignar el id.

La forma obvia de evitar esto es simplemente no agregar el objeto a colecciones basadas en hash antes de asignar la identificación. Entiendo que esto no siempre es posible porque es posible que desee deduplicar antes de asignar la identificación ya. Para poder seguir utilizando las colecciones basadas en hash, simplemente tiene que reconstruir las colecciones después de asignar la identificación.

Podrías hacer algo como esto:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

No he probado el enfoque exacto, así que no estoy seguro de cómo funciona el cambio de colecciones en eventos pre y post persistencia, pero la idea es:

  • Eliminar temporalmente el objeto de 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. Yo personalmente uso el enfoque basado en secuencia la mayor parte del tiempo y solo uso el enfoque UUID si necesito exponer un identificador a sistemas externos.


0

Con el nuevo estilo de instanceofjava 14, puede implementar el equalsen una línea.

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

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:

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

de esta manera obtendríamos una clave primaria predeterminada para la entidad del proveedor de persistencia, y nuestras funciones hashCode () y equals () podrían confiar en eso.

También podríamos declarar protegidos a los constructores del automóvil y luego usar la reflexión en nuestro método comercial para acceder a ellos. De esta forma, los desarrolladores no tendrían la intención de instanciar Car con nuevo, sino a través del método de fábrica.

¿Qué tal eso?


Un enfoque que funciona muy bien si está dispuesto a aprovechar el rendimiento tanto para generar el guid al hacer una búsqueda en la base de datos.
Michael Wiles

1
¿Qué pasa con la unidad de prueba de coche? En este caso, ¿necesita una conexión de base de datos para realizar pruebas? Además, sus objetos de dominio no deberían depender de la persistencia.
jhegedus

-1

Traté de responder esta pregunta yo mismo y nunca estuve totalmente contento con las soluciones encontradas hasta que leí esta publicación y especialmente DREW. Me gustó la forma en que perezoso creó UUID y lo almacenó de manera óptima.

Pero quería agregar aún más flexibilidad, es decir, crear lazy UUID SOLAMENTE 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"
  • use la identificación de la base de datos tanto como sea posible porque ¿por qué haría el trabajo dos veces?
  • evite problemas al acceder a hashCode () / equals () en una entidad aún no persistente y mantenga el mismo comportamiento después de que realmente persista

Realmente agradecería comentarios sobre mi solución mixta a continuación

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

¿Qué quiere decir con "UUID generado un día en esta entidad antes de que persistiera"? ¿podría dar un ejemplo para este caso?
jhegedus

¿podría usar el tipo de generación asignado? ¿Por qué se necesita el tipo de generación de identidad? ¿Tiene alguna ventaja sobre asignado?
jhegedus

qué sucede si 1) crea un MyEntity nuevo, 2) lo coloca en una lista, 3) luego lo guarda en la base de datos y luego 4) carga esa entidad de nuevo desde la base de datos y 5) intenta ver si la instancia cargada está en la lista . Supongo que no será así, aunque debería serlo.
jhegedus

Gracias por sus primeros comentarios que me mostraron que no estaba tan claro como debería. En primer lugar, "UUID un día generado en esta entidad antes de que persistiera" era un error tipográfico ... "antes de que persistiera IT" debería haberse leído en su lugar. Para los otros comentarios, editaré mi publicación pronto para tratar de explicar mejor mi solución.
user2083808

-1

En la práctica, parece que la opción 2 (clave primaria) se usa con mayor frecuencia. La clave comercial natural e INMUTABLE rara vez es algo, la creación y el soporte de claves sintéticas son demasiado pesadas para resolver situaciones, que probablemente nunca sucedan. Eche un vistazo a la implementación Spring-data-jpa AbstractPersistable (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 nuevos objetos en HashSet / HashMap. Por el contrario, la Opción 1 ( Objectimplementación restante ) 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 hashCodea constante, como se indicó a continuación Vlad Mihalcea.


-2

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 UUIDEntity, por lo que sigo el principio de no repetir (DRY).

  • Si es necesario, la generación de UUID se puede hacer más precisa (mediante el uso de 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()
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.