Il y a eu quelques discussions ici sur les entités JPA et qui hashCode()
/equals()
implémentation à utiliser pour les classes d'entités JPA. La plupart (sinon la totalité) d'entre eux dépendent d'Hibernate, mais j'aimerais en discuter de manière neutre sur l'implémentation JPA (j'utilise EclipseLink, soit dit en passant).
Toutes les implémentations possibles ont leurs propres avantages et inconvénients concernant:
hashCode()
/equals()
conformité du contrat (immuabilité) pourList
/Set
opérations- Qu'ils soient identiques objets (par exemple de sessions différentes, des proxys dynamiques provenant de structures de données chargées paresseusement) peuvent être détectés
- Si les entités se comportent correctement dans un état détaché (ou non persistant)
Pour autant que je puisse voir, il y a trois options :
- Ne les remplacez pas; compter sur
Object.equals()
etObject.hashCode()
hashCode()
/equals()
travail- impossible d'identifier des objets identiques, problèmes avec les proxys dynamiques
- aucun problème avec les entités détachées
- Remplacez-les, en fonction de la clé primaire
hashCode()
/equals()
sont cassés- identité correcte (pour toutes les entités gérées)
- problèmes avec des entités détachées
- Remplacez-les, en fonction de l' ID d'entreprise (champs de clé non primaire; qu'en est-il des clés étrangères?)
hashCode()
/equals()
sont cassés- identité correcte (pour toutes les entités gérées)
- aucun problème avec les entités détachées
Mes questions sont:
- Ai-je raté une option et / ou un point pro / con?
- Quelle option avez-vous choisie et pourquoi?
MISE À JOUR 1:
Par " hashCode()
/ equals()
sont rompus", je veux dire que des hashCode()
invocations successives peuvent renvoyer des valeurs différentes, ce qui (lorsqu'elles sont correctement implémentées) n'est pas rompu au sens de la Object
documentation de l' API, mais qui provoque des problèmes lors de la tentative de récupération d'une entité modifiée à partir d'un Map
, Set
ou autre basé sur le hachage Collection
. Par conséquent, les implémentations JPA (au moins EclipseLink) ne fonctionneront pas correctement dans certains cas.
MISE À JOUR 2:
Merci pour vos réponses - la plupart ont une qualité remarquable.
Malheureusement, je ne sais toujours pas quelle approche sera la meilleure pour une application réelle, ou comment déterminer la meilleure approche pour mon application. Donc, je garderai la question ouverte et j'espère avoir d'autres discussions et / ou opinions.
hashcode()
à la même instance d'objet devrait retourner la même valeur, à moins que les champs utilisés dans laequals()
mise en œuvre ne changent. En d'autres termes, si vous avez trois champs dans votre classe et que votreequals()
méthode n'en utilise que deux pour déterminer l'égalité des instances, vous pouvez vous attendre à ce que lahashcode()
valeur de retour change si vous modifiez l'une de ces valeurs de champ - ce qui est logique lorsque vous considérez que cette instance d'objet n'est plus "égale" à la valeur que l'ancienne instance représentait.Réponses:
Lisez ce très bel article sur le sujet: Ne laissez pas Hibernate voler votre identité .
La conclusion de l'article se présente comme suit:
la source
suid.js
Récupère essentiellement les blocs d'identification à partirsuid-server-java
desquels vous pouvez ensuite obtenir et utiliser côté client.Je remplace toujours equals / hashcode et je l'implémente en fonction de l'ID de l'entreprise. Semble la solution la plus raisonnable pour moi. Voir le lien suivant .
MODIFIER :
Pour expliquer pourquoi cela fonctionne pour moi:
la source
Nous avons généralement deux identifiants dans nos entités:
equals()
ethashCode()
en particulier)Regarde:
EDIT: pour clarifier mon point concernant les appels à la
setUuid()
méthode. Voici un scénario typique:Lorsque j'exécute mes tests et que je vois la sortie du journal, je résout le problème:
Alternativement, on peut fournir un constructeur distinct:
Donc, mon exemple ressemblerait à ceci:
J'utilise un constructeur par défaut et un setter, mais vous pouvez trouver une approche à deux constructeurs plus appropriée pour vous.
la source
hashCode
/ par défautequals
pour l'égalité JVM et l'égalitéid
de persistance? Cela n'a aucun sens pour moi.Object
« sequals()
retourneraientfalse
dans ce cas.equals()
Retours basés sur UUIDtrue
.Personnellement, j'ai déjà utilisé ces trois stratégies dans différents projets. Et je dois dire que l'option 1 est à mon avis la plus pratique dans une application réelle. D'après mon expérience, briser la conformité hashCode () / equals () conduit à de nombreux bugs fous, car vous vous retrouverez à chaque fois dans des situations où le résultat de l'égalité change après qu'une entité a été ajoutée à une collection.
Mais il existe d'autres options (également avec leurs avantages et leurs inconvénients):
a) hashCode / est égale à la base d'un ensemble d' immuable , non nulle , constructeur assigné champs
(+) les trois critères sont garantis
(-) les valeurs de champ doivent être disponibles pour créer une nouvelle instance
(-) complique la manipulation si vous devez en changer un
b) hashCode / equals basé sur une clé primaire assignée par l'application (dans le constructeur) au lieu de JPA
(+) les trois critères sont garantis
(-) vous ne pouvez pas profiter de simples stratégies de génération d'ID fiables comme les séquences de bases de données
(-) compliqué si de nouvelles entités sont créées dans un environnement distribué (client / serveur) ou un cluster de serveurs d'applications
c) hashCode / equals basé sur un UUID attribué par le constructeur de l'entité
(+) les trois critères sont garantis
(-) frais généraux de génération UUID
(-) peut être un petit risque que deux fois le même UUID soit utilisé, selon l'algorithme utilisé (peut être détecté par un index unique sur DB)
la source
Collection
.Si vous souhaitez utiliser
equals()/hashCode()
pour vos ensembles, dans le sens où la même entité ne peut y être qu'une seule fois, il n'y a qu'une seule option: Option 2. C'est parce qu'une clé primaire pour une entité par définition ne change jamais (si quelqu'un met effectivement à jour ce n'est plus la même entité)Vous devez le prendre au pied de la lettre: étant donné que vous
equals()/hashCode()
êtes basé sur la clé primaire, vous ne devez pas utiliser ces méthodes tant que la clé primaire n'est pas définie. Vous ne devez donc pas mettre d'entités dans l'ensemble, jusqu'à ce qu'elles se voient attribuer une clé primaire. (Oui, les UUID et les concepts similaires peuvent aider à attribuer les clés primaires au début.)Maintenant, il est théoriquement possible d'y parvenir avec l'option 3, même si les soi-disant "clés d'entreprise" ont l'inconvénient désagréable qu'elles peuvent changer: "Tout ce que vous aurez à faire est de supprimer les entités déjà insérées de l'ensemble ( s) et réinsérez-les. " C'est vrai - mais cela signifie également que, dans un système distribué, vous devrez vous assurer que cela se fait absolument partout où les données ont été insérées (et vous devrez vous assurer que la mise à jour est effectuée , avant que d'autres choses ne se produisent). Vous aurez besoin d'un mécanisme de mise à jour sophistiqué, surtout si certains systèmes distants ne sont pas actuellement accessibles ...
L'option 1 ne peut être utilisée que si tous les objets de vos ensembles proviennent de la même session Hibernate. La documentation d'Hibernate le montre très clairement au chapitre 13.1.3. Considérant l'identité de l'objet :
Il continue de plaider en faveur de l'option 3:
C'est vrai, si vous
Sinon, vous êtes libre de choisir l'option 2.
Il mentionne ensuite la nécessité d'une relative stabilité:
C'est correct. Le problème pratique que je vois avec ceci est: si vous ne pouvez pas garantir une stabilité absolue, comment pourrez-vous garantir la stabilité "tant que les objets sont dans le même ensemble". Je peux imaginer des cas particuliers (comme utiliser des ensembles uniquement pour une conversation et ensuite les jeter), mais je remets en question la faisabilité générale de cela.
Version courte:
la source
equals
/hashCode
.Object
implémentations égales et hashCode par défaut car cela ne fonctionne pas après vousmerge
et l'entité.Vous pouvez utiliser l'identifiant d'entité comme suggéré dans ce post . Le seul problème est que vous devez utiliser une
hashCode
implémentation qui renvoie toujours la même valeur, comme ceci:la source
Bien que l'utilisation d'une clé métier (option 3) soit l'approche la plus couramment recommandée ( wiki de la communauté Hibernate , "Java Persistence with Hibernate" p. 398), et c'est ce que nous utilisons le plus souvent, il y a un bogue Hibernate qui rompt cela pour une recherche rapide. ensembles: HHH-3799 . Dans ce cas, Hibernate peut ajouter une entité à un ensemble avant que ses champs soient initialisés. Je ne sais pas pourquoi ce bogue n'a pas attiré plus d'attention, car cela rend vraiment problématique l'approche clé commerciale recommandée.
Je pense que le cœur du problème est que equals et hashCode doivent être basés sur un état immuable (référence Odersky et al. ), Et une entité Hibernate avec une clé primaire gérée par Hibernate n'a pas un tel état immuable. La clé primaire est modifiée par Hibernate lorsqu'un objet transitoire devient persistant. La clé métier est également modifiée par Hibernate, lorsqu'elle hydrate un objet en cours d'initialisation.
Cela ne laisse que l'option 1, héritant des implémentations java.lang.Object basées sur l'identité de l'objet, ou utilisant une clé primaire gérée par l'application comme suggéré par James Brundege dans "Don't Let Hibernate Steal Your Identity" (déjà référencé par la réponse de Stijn Geukens ) et par Lance Arlaus dans «Object Generation: A Better Approach to Hibernate Integration» .
Le plus gros problème avec l'option 1 est que les instances détachées ne peuvent pas être comparées aux instances persistantes utilisant .equals (). Mais ça va; le contrat entre égaux et hashCode laisse au développeur le soin de décider ce que signifie l'égalité pour chaque classe. Il suffit donc de laisser equals et hashCode hériter de Object. Si vous devez comparer une instance détachée à une instance persistante, vous pouvez créer une nouvelle méthode explicitement à cet effet, peut-être
boolean sameEntity
ouboolean dbEquivalent
ouboolean businessEquals
.la source
Je suis d'accord avec la réponse d'Andrew. Nous faisons la même chose dans notre application, mais au lieu de stocker les UUID en tant que VARCHAR / CHAR, nous le divisons en deux valeurs longues. Voir UUID.getLeastSignificantBits () et UUID.getMostSignificantBits ().
Une autre chose à considérer est que les appels à UUID.randomUUID () sont assez lents, vous pouvez donc envisager de générer paresseusement l'UUID uniquement lorsque cela est nécessaire, comme pendant la persistance ou les appels à equals () / hashCode ()
la source
Comme d'autres personnes l'ont déjà fait de façon plus intelligente que moi, il existe de nombreuses stratégies. Il semble cependant que la majorité des modèles de conception appliqués tentent de pirater leur chemin vers le succès. Ils limitent l'accès des constructeurs sinon gênent complètement les invocations de constructeurs avec des constructeurs spécialisés et des méthodes d'usine. En effet, il est toujours agréable avec une API claire. Mais si la seule raison est de rendre les remplacements de codes égaux et de hachage compatibles avec l'application, alors je me demande si ces stratégies sont conformes à KISS (Keep It Simple Stupid).
Pour moi, j'aime surcharger equals et hashcode en examinant l'identifiant. Dans ces méthodes, j'ai besoin que l'id ne soit pas nul et documente bien ce comportement. Ainsi, il deviendra le contrat des développeurs pour conserver une nouvelle entité avant de le stocker ailleurs. Une application qui n'honore pas ce contrat échouerait dans la minute (espérons-le).
Attention cependant: si vos entités sont stockées dans des tables différentes et que votre fournisseur utilise une stratégie de génération automatique pour la clé primaire, vous obtiendrez des clés primaires dupliquées entre les types d'entités. Dans ce cas, comparez également les types d'exécution avec un appel à Object # getClass (), ce qui rendra bien sûr impossible que deux types différents soient considérés comme égaux. Cela me convient très bien pour la plupart.
la source
Object#getClass()
est mauvais à cause de H. procurations. L'appelHibernate.getClass(o)
aide, mais le problème de l'égalité des entités de différents types demeure. Il existe une solution utilisant canEqual , un peu compliquée, mais utilisable. Je suis d'accord pour dire que ce n'est généralement pas nécessaire. +++ Lancement en eq / hc sur null ID viole le contrat, mais c'est très pragmatique.Il y a évidemment déjà des réponses très instructives ici mais je vais vous dire ce que nous faisons.
Nous ne faisons rien (c'est-à-dire que nous ne remplaçons pas).
Si nous avons besoin d'equals / hashcode pour travailler pour les collections, nous utilisons des UUID. Vous venez de créer l'UUID dans le constructeur. Nous utilisons http://wiki.fasterxml.com/JugHome pour UUID. L'UUID est un peu plus cher en termes de CPU, mais il est bon marché par rapport à la sérialisation et à l'accès DB.
la source
J'ai toujours utilisé l'option 1 dans le passé parce que j'étais au courant de ces discussions et pensais qu'il valait mieux ne rien faire jusqu'à ce que je sache la bonne chose à faire. Ces systèmes fonctionnent toujours avec succès.
Cependant, la prochaine fois, je peux essayer l'option 2 - en utilisant l'ID généré par la base de données.
Hashcode et equals lèveront IllegalStateException si l'ID n'est pas défini.
Cela évitera que des erreurs subtiles impliquant des entités non enregistrées n'apparaissent de manière inattendue.
Que pensent les gens de cette approche?
la source
L'approche des clés métier ne nous convient pas. Nous utilisons l' ID généré par la base de données , tempId transitoire temporaire et redéfinissons equal () / hashcode () pour résoudre le dilemme. Toutes les entités sont des descendants de l'entité. Avantages:
Les inconvénients:
Regardez notre code:
la source
Veuillez considérer l'approche suivante basée sur l'identifiant de type prédéfini et l'ID.
Les hypothèses spécifiques pour JPA:
L'entité abstraite:
Exemple d'entité concrète:
Exemple de test:
Principaux avantages ici:
Désavantages:
super()
Remarques:
class A
etclass B extends A
peut dépendre de détails concrets de l'application.Attendant vos commentaires avec hâte.
la source
Il s'agit d'un problème courant dans tous les systèmes informatiques qui utilisent Java et JPA. Le point difficile s'étend au-delà de l'implémentation de equals () et hashCode (), il affecte la façon dont une organisation se réfère à une entité et comment ses clients se réfèrent à la même entité. J'ai vu assez de douleur de ne pas avoir de clé commerciale au point que j'ai écrit mon propre blog pour exprimer mon point de vue.
En bref: utilisez un identifiant séquentiel court, lisible par l'homme avec des préfixes significatifs comme clé métier généré sans aucune dépendance sur un stockage autre que la RAM. Le Snowflake de Twitter en est un très bon exemple.
la source
OMI, vous avez 3 options pour implémenter equals / hashCode
L'utilisation d'une identité générée par l'application est l'approche la plus simple, mais présente quelques inconvénients
Si vous pouvez travailler avec ces inconvénients , utilisez simplement cette approche.
Pour surmonter le problème de jointure, vous pouvez utiliser l'UUID comme clé naturelle et une valeur de séquence comme clé primaire, mais vous pouvez toujours rencontrer les problèmes d'implémentation equals / hashCode dans les entités enfant de composition qui ont des identifiants intégrés, car vous voudrez les rejoindre en fonction sur la clé primaire. L'utilisation de la clé naturelle dans l'ID d'entité enfant et de la clé primaire pour faire référence au parent est un bon compromis.
IMO, c'est l'approche la plus propre car elle évitera tous les inconvénients et vous fournira en même temps une valeur (l'UUID) que vous pouvez partager avec des systèmes externes sans exposer les internes du système.
Implémentez-le sur la base d'une clé métier si vous pouvez vous attendre à ce qu'un utilisateur soit une bonne idée, mais présente également quelques inconvénients
La plupart du temps, cette clé métier sera une sorte de code fourni par l'utilisateur et moins souvent un composite de plusieurs attributs.
OMI, vous ne devez pas implémenter ou travailler exclusivement avec une clé métier. C'est un bon complément, c'est-à-dire que les utilisateurs peuvent rapidement rechercher par cette clé métier, mais le système ne doit pas compter dessus pour fonctionner.
L'implémenter en fonction de la clé primaire a ses problèmes, mais ce n'est peut-être pas si grave
Si vous avez besoin d'exposer des identifiants à un système externe, utilisez l'approche UUID que j'ai suggérée. Si vous ne le faites pas, vous pouvez toujours utiliser l'approche UUID, mais ce n'est pas obligatoire. Le problème de l'utilisation d'un ID généré par SGBD dans equals / hashCode provient du fait que l'objet peut avoir été ajouté à des collections basées sur le hachage avant d'attribuer l'ID.
Le moyen évident de contourner ce problème consiste simplement à ne pas ajouter l'objet aux collections basées sur le hachage avant d'attribuer l'ID. Je comprends que ce n'est pas toujours possible car vous voudrez peut-être la déduplication avant d'attribuer déjà l'identifiant. Pour continuer à utiliser les collections basées sur le hachage, il vous suffit de reconstruire les collections après avoir attribué l'ID.
Vous pouvez faire quelque chose comme ça:
Je n'ai pas testé l'approche exacte moi-même, donc je ne sais pas comment fonctionne la modification des collections dans les événements pré et post-persistants, mais l'idée est la suivante:
Une autre façon de résoudre ce problème est de simplement reconstruire tous vos modèles basés sur le hachage après une mise à jour / persistance.
En fin de compte, cela dépend de vous. J'utilise personnellement l'approche basée sur la séquence la plupart du temps et n'utilise l'approche UUID que si j'ai besoin d'exposer un identifiant à des systèmes externes.
la source
Avec le nouveau style de
instanceof
from java 14, vous pouvez implémenter leequals
en une seule ligne.la source
Si l'UUID est la réponse pour beaucoup de gens, pourquoi ne pas simplement utiliser les méthodes d'usine de la couche métier pour créer les entités et attribuer la clé primaire au moment de la création?
par exemple:
De cette façon, nous obtiendrions une clé primaire par défaut pour l'entité auprès du fournisseur de persistance, et nos fonctions hashCode () et equals () pourraient s'appuyer sur cela.
Nous pourrions également déclarer les constructeurs de la Voiture protégés, puis utiliser la réflexion dans notre méthode commerciale pour y accéder. De cette façon, les développeurs n'auraient pas l'intention d'instancier Car avec une nouvelle méthode, mais via la méthode d'usine.
Comment ça?
la source
J'ai essayé de répondre à cette question moi-même et je n'ai jamais été totalement satisfait des solutions trouvées avant d'avoir lu cet article et en particulier celui de DREW. J'ai aimé la façon dont il a créé paresseusement l'UUID et l'a stocké de manière optimale.
Mais je voulais ajouter encore plus de flexibilité, c'est-à-dire créer paresseusement UUID UNIQUEMENT lors de l'accès à hashCode () / equals () avant la première persistance de l'entité avec les avantages de chaque solution:
J'apprécierais vraiment les commentaires sur ma solution mixte ci-dessous
la source
En pratique, il semble que l'option 2 (clé primaire) soit la plus fréquemment utilisée. La clé commerciale naturelle et IMMUTABLE est rarement chose, la création et la prise en charge de clés synthétiques sont trop lourdes pour résoudre des situations, qui ne se sont probablement jamais produites. Jetez un coup d'œil à l' implémentation Abstract-Persistable de spring-data-jpa (la seule chose: pour l'utilisation de l'implémentation Hibernate
Hibernate.getClass
).Juste conscient de la manipulation de nouveaux objets dans HashSet / HashMap. En revanche, l'option 1 (rester
Object
mise en œuvre) est rompue juste aprèsmerge
, c'est une situation très courante.Si vous n'avez pas de clé commerciale et que vous avez vraiment besoin de manipuler une nouvelle entité dans la structure de hachage, remplacez-la
hashCode
par constante, comme indiqué ci-dessous, Vlad Mihalcea a été conseillé.la source
Vous trouverez ci-dessous une solution simple (et testée) pour Scala.
Notez que cette solution ne rentre dans aucune des 3 catégories données dans la question.
Toutes mes entités sont des sous-classes de l'UUIDEntity, donc je respecte le principe de non-répétition (DRY).
Si nécessaire, la génération d'UUID peut être rendue plus précise (en utilisant plus de nombres pseudo-aléatoires).
Code Scala:
la source