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) paraList
/Set
operaciones- 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 :
- 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
- Anularlos, según la clave primaria
hashCode()
/equals()
están rotos- identidad correcta (para todas las entidades gestionadas)
- problemas con entidades separadas
- 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:
- ¿Perdí una opción y / o un punto pro / con?
- ¿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 Object
documentación de la API, pero que causa problemas al intentar recuperar una entidad cambió de una Map
, Set
u 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.
hashcode()
a la misma instancia de objeto debería devolver el mismo valor, a menos queequals()
cambie cualquier campo utilizado en la implementación. En otras palabras, si tiene tres campos en su clase y suequals()
método usa solo dos de ellos para determinar la igualdad de instancias, entonces puede esperar que elhashcode()
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.Respuestas:
Lea este bonito artículo sobre el tema: No deje que Hibernate le robe su identidad .
La conclusión del artículo es así:
fuente
suid.js
recupera bloques de identificación de lossuid-server-java
que luego puede obtener y usar el lado del cliente.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 .
EDITAR :
Para explicar por qué esto funciona para mí:
fuente
Generalmente tenemos dos ID en nuestras entidades:
equals()
yhashCode()
en particular)Echar un vistazo:
EDITAR: para aclarar mi punto con respecto a las llamadas al
setUuid()
método. Aquí hay un escenario típico:Cuando ejecuto mis pruebas y veo la salida del registro, soluciono el problema:
Alternativamente, uno puede proporcionar un constructor separado:
Entonces mi ejemplo se vería así:
Utilizo un constructor predeterminado y un setter, pero puede encontrar el enfoque de dos constructores más adecuado para usted.
fuente
hashCode
/ predeterminadosequals
para la igualdad de JVM yid
para la igualdad de persistencia? Esto no tiene sentido para mí en absoluto.Object
'sequals()
volveríafalse
en este caso. UUID basados enequals()
rendimientostrue
.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)
fuente
Collection
.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 :
Sigue argumentando a favor de la Opción 3:
Esto es cierto, si usted
De lo contrario, puede elegir la Opción 2.
Luego menciona la necesidad de una relativa estabilidad:
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:
fuente
equals
/hashCode
.Object
implementaciones predeterminadas de igual y hashCode porque eso no funciona después de ustedmerge
y la entidad.Puede usar el identificador de entidad como se sugiere en esta publicación . El único inconveniente es que necesita usar una
hashCode
implementación que siempre devuelva el mismo valor, como este:fuente
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 sameEntity
oboolean dbEquivalent
oboolean businessEquals
.fuente
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 ()
fuente
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.
fuente
Object#getClass()
es malo debido a los representantes de H. LlamarHibernate.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.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.
fuente
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?
fuente
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:
Contras:
Mira nuestro código:
fuente
Considere el siguiente enfoque basado en el identificador de tipo predefinido y la ID.
Los supuestos específicos para JPA:
La entidad abstracta:
Ejemplo de entidad concreta:
Ejemplo de prueba:
Principales ventajas aquí:
Desventajas
super()
Notas:
class A
yclass B extends A
puede depender de detalles concretos de la aplicación.Esperamos sus comentarios.
fuente
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.
fuente
En mi opinión, tiene 3 opciones para implementar equals / hashCode
El uso de una identidad generada por la aplicación es el enfoque más fácil, pero tiene algunas desventajas
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.
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.
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:
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:
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.
fuente
Con el nuevo estilo de
instanceof
java 14, puede implementar elequals
en una línea.fuente
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:
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?
fuente
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:
Realmente agradecería comentarios sobre mi solución mixta a continuación
fuente
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 Hibernate
Hibernate.getClass
).Solo consciente de manipular nuevos objetos en HashSet / HashMap. Por el contrario, la Opción 1 (
Object
implementación restante ) se rompe justo despuésmerge
, 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.fuente
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:
fuente