Le dilemme JPA hashCode () / equals ()

311

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é) pour List/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 :

  1. 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
  2. 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
  3. 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:

  1. Ai-je raté une option et / ou un point pro / con?
  2. 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 Objectdocumentation 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, Setou 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.

MRalwasser
la source
4
Je ne comprends pas ce que vous entendez par "hashCode () / equals () broken"
nanda
4
Ils ne seraient alors pas "cassés" dans ce sens, comme dans les options 2 et 3, vous implémenteriez equals () et hashCode () en utilisant la même stratégie.
mat le
11
Ce n'est pas le cas pour l'option 3. hashCode () et equals () devraient utiliser les mêmes critères, donc si l'un de vos champs change, oui la méthode hashcode () renverra une valeur différente pour la même instance que la précédente, mais il en sera de même pour (). Vous avez laissé la deuxième partie de la phrase du javadoc hashcode (): chaque fois qu'elle est invoquée plusieurs fois sur le même objet lors de l'exécution d'une application Java, la méthode hashCode doit toujours renvoyer le même entier, à condition qu'aucune information utilisé dans des comparaisons égales sur l'objet est modifié .
mat le
1
En fait, cette partie de la phrase signifie le contraire - l'appel hashcode()à la même instance d'objet devrait retourner la même valeur, à moins que les champs utilisés dans la equals()mise en œuvre ne changent. En d'autres termes, si vous avez trois champs dans votre classe et que votre equals()méthode n'en utilise que deux pour déterminer l'égalité des instances, vous pouvez vous attendre à ce que la hashcode()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.
mat le
2
"problèmes lors de la tentative de récupération d'une entité modifiée à partir d'une carte, d'un ensemble ou d'autres collections basées sur le hachage" ... cela devrait être "des problèmes lors de la tentative de récupération d'une entité modifiée à partir d'un HashMap, d'un HashSet ou d'autres collections basées sur le hachage"
nanda

Réponses:

122

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:

L'identité d'objet est trompeusement difficile à implémenter correctement lorsque les objets sont conservés dans une base de données. Cependant, les problèmes proviennent entièrement de la possibilité d'exister des objets sans identifiant avant qu'ils ne soient enregistrés. Nous pouvons résoudre ces problèmes en prenant la responsabilité d'attribuer des ID d'objet aux cadres de mappage relationnel objet tels que Hibernate. Au lieu de cela, les ID d'objet peuvent être attribués dès que l'objet est instancié. Cela rend l'identité d'objet simple et sans erreur, et réduit la quantité de code nécessaire dans le modèle de domaine.

Stijn Geukens
la source
21
Non, ce n'est pas un bel article. C'est un excellent article sur le sujet, et il devrait être lu pour tous les programmeurs JPA! +1!
Tom Anderson
2
Ouais j'utilise la même solution. Ne pas laisser la base de données générer l'ID présente également d'autres avantages, comme la possibilité de créer un objet et de créer déjà d'autres objets qui le référencent avant de le persister. Cela peut supprimer la latence et les cycles de demande / réponse multiples dans les applications client-serveur. Si vous avez besoin d'inspiration pour une telle solution, consultez mes projets: suid.js et suid-server-java . suid.jsRécupère essentiellement les blocs d'identification à partir suid-server-javadesquels vous pouvez ensuite obtenir et utiliser côté client.
Stijn de Witt
2
C'est tout simplement fou. Je suis nouveau en hibernation des travaux sous le capot, j'écrivais des tests unitaires et j'ai découvert que je ne pouvais pas supprimer un objet d'un ensemble après l'avoir modifié, j'ai conclu que c'était à cause du changement de code de hachage, mais je n'ai pas pu comprendre comment résoudre. L'article est tout simplement magnifique!
XMight
C'est un excellent article. Cependant, pour les personnes qui voient le lien pour la première fois, je dirais que cela pourrait être exagéré pour la plupart des applications. Les 3 autres options répertoriées sur cette page devraient plus ou moins résoudre le problème de plusieurs manières.
HopeKing
1
Hibernate / JPA utilise-t-il la méthode equals et hashcode d'une entité pour vérifier si l'enregistrement existe déjà dans la base de données?
Tushar Banne
64

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 .

Pour résumer tout cela, voici une liste de ce qui fonctionnera ou ne fonctionnera pas avec les différentes façons de gérer equals / hashCode: entrez la description de l'image ici

MODIFIER :

Pour expliquer pourquoi cela fonctionne pour moi:

  1. Je n'utilise généralement pas de collection basée sur le hachage (HashMap / HashSet) dans mon application JPA. Si je le dois, je préfère créer une solution UniqueList.
  2. Je pense que la modification de l'ID de l'entreprise lors de l'exécution n'est pas une bonne pratique pour toute application de base de données. Dans de rares cas où il n'y a pas d'autre solution, je ferais un traitement spécial comme supprimer l'élément et le remettre dans la collection basée sur le hachage.
  3. Pour mon modèle, j'ai défini l'ID métier sur le constructeur et ne lui ai pas fourni de setters. Je laisse l'implémentation JPA changer le champ au lieu de la propriété.
  4. La solution UUID semble exagérée. Pourquoi UUID si vous avez un identifiant commercial naturel? Après tout, je définirais l'unicité de l'ID d'entreprise dans la base de données. Pourquoi avoir alors TROIS index pour chaque table de la base de données?
nanda
la source
1
Mais il manque à cette table une cinquième ligne "fonctionne avec List / Sets" (si vous pensez à supprimer une entité qui fait partie d'un ensemble d'un mappage OneToMany) à laquelle il serait répondu "Non" sur les deux dernières options car son hashCode ( ) modifications qui violent son contrat.
MRalwasser
Voir le commentaire sur la question. Vous semblez mal comprendre le contrat equals / hashcode
nanda
1
@MRalwasser: Je pense que vous voulez dire la bonne chose, ce n'est tout simplement pas le contrat equals / hashCode () lui-même qui est violé. Mais un mutable equals / hashCode crée des problèmes avec le contrat Set .
Chris Lercher
3
@MRalwasser: Le code de hachage ne peut changer que si l'ID de l'entreprise change, et le fait est que l'ID de l'entreprise ne change pas . Le hashcode ne change donc pas, et cela fonctionne parfaitement avec les collections hachées.
Tom Anderson
1
Et si vous n'avez pas de clé commerciale naturelle? Par exemple, dans le cas d'un point bidimensionnel, Point (X, Y), dans une application de dessin graphique? Comment stockeriez-vous ce point en tant qu'entité?
jhegedus
35

Nous avons généralement deux identifiants dans nos entités:

  1. Ne concerne que la couche de persistance (afin que le fournisseur de persistance et la base de données puissent déterminer les relations entre les objets).
  2. Est pour nos besoins d'application ( equals()et hashCode()en particulier)

Regarde:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

EDIT: pour clarifier mon point concernant les appels à la setUuid()méthode. Voici un scénario typique:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("[email protected]");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

Lorsque j'exécute mes tests et que je vois la sortie du journal, je résout le problème:

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

Alternativement, on peut fournir un constructeur distinct:

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

Donc, mon exemple ressemblerait à ceci:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

J'utilise un constructeur par défaut et un setter, mais vous pouvez trouver une approche à deux constructeurs plus appropriée pour vous.

Andrew Андрей Листочкин
la source
2
Je pense que c'est une bonne et bonne solution. Il peut également avoir un petit avantage en termes de performances, car les entiers fonctionnent généralement mieux dans les index de base de données que les uuids. Mais à part cela, vous pourriez probablement éliminer la propriété id d'entier actuelle et la remplacer par l'uuid (attribué par l'application)?
Chris Lercher
4
En quoi est-ce différent de l'utilisation des méthodes hashCode/ par défaut equalspour l'égalité JVM et l'égalité idde persistance? Cela n'a aucun sens pour moi.
Behrang Saeedzadeh
2
Cela fonctionne dans les cas où plusieurs objets entité pointent vers la même ligne dans une base de données. Object« s equals()retourneraient falsedans ce cas. equals()Retours basés sur UUID true.
Andrew Андрей Листочкин
4
-1 - Je ne vois aucune raison d'avoir deux identifiants, donc deux types d'identité. Cela me semble complètement inutile et potentiellement dangereux.
Tom Anderson
1
Désolé de critiquer votre solution sans en indiquer une que je préférerais. En bref, je donnerais aux objets un seul champ ID, j'implémenterais equals et hashCode en fonction, et je générerais sa valeur lors de la création d'objet, plutôt que lors de l'enregistrement dans la base de données. De cette façon, toutes les formes de l'objet fonctionnent de la même manière: non persistantes, persistantes et détachées. Les proxys Hibernate (ou similaires) devraient également fonctionner correctement, et je pense qu'ils n'auraient même pas besoin d'être hydratés pour gérer les appels égaux et hashCode.
Tom Anderson
31

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)

lweller
la source
Je suis fan de l' option 1 et approche C également. Ne rien faire jusqu'à ce que vous en ayez absolument besoin est l'approche la plus agile.
Adam Gent
2
+1 pour l'option (b). À mon humble avis, si une entité a un ID d'entreprise naturel, cela devrait également être sa clé primaire de base de données. C'est une conception de base de données simple, directe et efficace. S'il n'a pas un tel ID, une clé de substitution est nécessaire. Si vous définissez cela lors de la création d'objet, tout le reste est simple. C'est lorsque les gens n'utilisent pas de clé naturelle et ne génèrent pas de clé de substitution au début qu'ils ont des problèmes. Quant à la complexité de la mise en œuvre - oui, il y en a. Mais vraiment pas beaucoup, et cela peut être fait d'une manière très générique qui le résout une fois pour toutes les entités.
Tom Anderson
Je préfère également l'option 1, mais alors comment écrire un test unitaire pour affirmer l'égalité complète est un gros problème, car nous devons implémenter la méthode égal pour Collection.
OOD Waterball
Ne le fais pas. Voir Don't Mess With The Hibernate
alonana
29

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 :

Dans une session, l'application peut utiliser en toute sécurité == pour comparer des objets.

Cependant, une application qui utilise == en dehors d'une session peut produire des résultats inattendus. Cela peut se produire même dans certains endroits inattendus. Par exemple, si vous placez deux instances détachées dans le même ensemble, les deux peuvent avoir la même identité de base de données (c'est-à-dire qu'elles représentent la même ligne). L'identité JVM, cependant, n'est par définition pas garantie pour les instances dans un état détaché. Le développeur doit remplacer les méthodes equals () et hashCode () dans les classes persistantes et implémenter leur propre notion d'égalité d'objet.

Il continue de plaider en faveur de l'option 3:

Il y a une mise en garde: n'utilisez jamais l'identifiant de la base de données pour implémenter l'égalité. Utilisez une clé métier qui est une combinaison d'attributs uniques, généralement immuables. L'identifiant de la base de données changera si un objet transitoire est rendu persistant. Si l'instance transitoire (généralement avec des instances détachées) est conservée dans un ensemble, la modification du code de hachage rompt le contrat de l'ensemble.

C'est vrai, si vous

  • ne peut pas attribuer l'ID tôt (par exemple en utilisant des UUID)
  • et pourtant vous voulez absolument mettre vos objets dans des ensembles pendant qu'ils sont dans un état transitoire.

Sinon, vous êtes libre de choisir l'option 2.

Il mentionne ensuite la nécessité d'une relative stabilité:

Les attributs des clés d'entreprise ne doivent pas nécessairement être aussi stables que les clés primaires de la base de données; il suffit de garantir la stabilité tant que les objets sont dans le même ensemble.

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:

  • L'option 1 ne peut être utilisée qu'avec des objets dans une seule session.
  • Si vous le pouvez, utilisez l'option 2. (Affectez PK le plus tôt possible, car vous ne pouvez pas utiliser les objets en ensembles tant que le PK n'est pas attribué.)
  • Si vous pouvez garantir une stabilité relative, vous pouvez utiliser l'option 3. Mais soyez prudent avec cela.
Chris Lercher
la source
Votre hypothèse selon laquelle la clé primaire ne change jamais est fausse. Par exemple, Hibernate alloue uniquement la clé primaire lorsque la session est enregistrée. Ainsi, si vous utilisez la clé primaire en tant que hashCode, le résultat de hashCode () avant d'enregistrer l'objet pour la première fois et après avoir enregistré l'objet sera différent. Pire, avant d'enregistrer la session, deux objets nouvellement créés auront le même hashCode et peuvent se remplacer lorsqu'ils sont ajoutés aux collections. Vous pouvez vous retrouver obligé de forcer une sauvegarde / vidage immédiatement lors de la création d'objet pour utiliser cette approche.
William Billingsley
2
@William: La clé primaire d'une entité ne change pas. La propriété id de l' objet mappé peut changer. Cela se produit, comme vous l'avez expliqué, en particulier lorsqu'un objet transitoire est rendu persistant . Veuillez lire attentivement la partie de ma réponse, où j'ai dit à propos des méthodes equals / hashCode: "vous ne devez pas utiliser ces méthodes tant que la clé primaire n'est pas définie."
Chris Lercher
Entièrement d'accord. Avec l'option 2, vous pouvez également factoriser equals / hashcode dans une super classe et le faire réutiliser par toutes vos entités.
Theo
+1 Je suis nouveau sur JPA, mais certains des commentaires et réponses ici impliquent que les gens ne comprennent pas la signification du terme "clé primaire".
Raedwald
16
  1. Si vous avez une clé d'entreprise , vous devez l'utiliser pour equals/ hashCode.
  2. Si vous n'avez pas de clé métier, vous ne devez pas la laisser avec les Objectimplémentations égales et hashCode par défaut car cela ne fonctionne pas après vous mergeet l'entité.
  3. Vous pouvez utiliser l'identifiant d'entité comme suggéré dans ce post . Le seul problème est que vous devez utiliser une hashCodeimplémentation qui renvoie toujours la même valeur, comme ceci:

    @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
    }
Vlad Mihalcea
la source
Quel est le meilleur: (1) onjava.com/pub/a/onjava/2006/09/13/… ou (2) vladmihalcea.com/… ? La solution (2) est plus facile que (1). Alors pourquoi devrais-je utiliser (1). Les effets des deux sont-ils les mêmes? Les deux garantissent-ils la même solution?
nimo23
Et avec votre solution: "la valeur hashCode ne change pas" entre les mêmes instances. Cela a le même comportement que s'il s'agissait du "même" uuide (de la solution (1)) comparé. Ai-je raison?
nimo23
1
Et stocker l'UUID dans la base de données et augmenter l'empreinte de l'enregistrement et dans le pool de mémoire tampon? Je pense que cela peut entraîner plus de problèmes de performances à long terme que le hashCode unique. Quant à l'autre solution, vous pouvez la vérifier pour voir si elle offre une cohérence à travers toutes les transitions d'état d'entité. Vous pouvez trouver le test qui vérifie cela sur GitHub .
Vlad Mihalcea
1
Si vous avez une clé d'entreprise immuable, le hashCode peut l'utiliser et il va bénéficier de plusieurs compartiments, donc cela vaut la peine d'être utilisé si vous en avez un. Sinon, utilisez simplement l'identifiant d'entité comme expliqué dans mon article.
Vlad Mihalcea
1
Je suis heureux que vous ayez aimé. J'ai des centaines d'autres articles sur JPA et Hibernate.
Vlad Mihalcea
10

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 sameEntityou boolean dbEquivalentou boolean businessEquals.

jbyler
la source
5

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

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

}
A dessiné
la source
Eh bien, en fait, si vous remplacez equals () / hashCode (), vous devez de toute façon générer un UUID pour chaque entité (je suppose que vous voulez conserver chaque entité que vous créez dans votre code). Vous ne le faites qu'une seule fois - avant de le stocker dans une base de données pour la première fois. Après cela, l'UUID est juste chargé par le fournisseur de persistance. Je ne vois donc pas l'intérêt de le faire paresseusement.
Andrew Андрей Листочкин
J'ai voté pour votre réponse parce que j'aime vraiment vos autres idées: stocker l'UUID sous la forme d'une paire de nombres dans la base de données et ne pas transtyper vers un type particulier à l'intérieur de la méthode equals () - celle-là est vraiment bien! Je vais certainement utiliser ces deux astuces à l'avenir.
Andrew Андрей Листочкин
1
Merci pour le vote positif. La raison de l'initialisation paresseuse de l'UUID était que dans notre application, nous créons beaucoup d'entités qui ne sont jamais mises dans un HashMap ou qui persistent. Nous avons donc constaté une baisse de performance de 100 fois lors de la création de l'objet (100 000 d'entre eux). Nous n'initialisons donc l'UUID qu'en cas de besoin. Je souhaite juste qu'il y ait un bon support dans MySql pour les nombres de 128 bits afin que nous puissions simplement utiliser l'UUID pour l'id aussi et ne nous soucions pas de l'auto_increment.
Drew
Oh je vois. Dans mon cas, nous ne déclarons même pas le champ UUID si l'entité correspondante ne va pas être placée dans les collections. L'inconvénient est que parfois nous devons l'ajouter car plus tard, il s'avère que nous devons en fait les mettre dans des collections. Cela se produit parfois pendant le développement, mais heureusement, il ne nous est jamais arrivé après le déploiement initial chez un client, ce n'était donc pas un gros problème. Si cela se produisait après la mise en service du système, nous aurions besoin d'une migration de base de données. Les UUID paresseux sont très utiles dans de telles situations.
Andrew Андрей Листочкин
Vous devriez peut-être également essayer le générateur d'UUID plus rapide suggéré par Adam dans sa réponse si les performances sont un problème critique dans votre situation.
Andrew Андрей Листочкин
3

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.

Martin Andersson
la source
Même avec une DB manquant des séquences (comme Mysql), il est possible de les simuler (par exemple, la table hibernate_sequence). Ainsi, vous pouvez toujours obtenir un ID unique sur toutes les tables. +++ Mais vous n'en avez pas besoin. L'appel Object#getClass() est mauvais à cause de H. procurations. L'appel Hibernate.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.
maaartinus
2

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.

Adam Gent
la source
1

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?

Neil Stevens
la source
1

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:

  1. Aucun champ supplémentaire dans DB
  2. Pas de codage supplémentaire dans les entités descendantes, une approche pour tous
  3. Pas de problèmes de performances (comme avec UUID), génération d'ID DB
  4. Pas de problème avec les Hashmaps (pas besoin de garder à l'esprit l'utilisation d'equal & etc.)
  5. Le code de hachage de la nouvelle entité ne change pas dans le temps même après avoir persisté

Les inconvénients:

  1. Il peut y avoir des problèmes avec la sérialisation et la désérialisation des entités non persistantes
  2. Le code de hachage de l'entité enregistrée peut changer après le rechargement à partir de la base de données
  3. Objets non persistants considérés comme toujours différents (peut-être que c'est vrai?)
  4. Quoi d'autre?

Regardez notre code:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}
Demel
la source
1

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:

  • les entités du même "type" et du même ID non nul sont considérées comme égales
  • les entités non persistantes (en supposant qu'aucun ID) ne sont jamais égales aux autres entités

L'entité abstraite:

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

Exemple d'entité concrète:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Exemple de test:

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

Principaux avantages ici:

  • simplicité
  • garantit que les sous-classes fournissent l'identité du type
  • comportement prévu avec des classes mandataires

Désavantages:

  • Nécessite que chaque entité appelle super()

Remarques:

  • A besoin d'attention lors de l'utilisation de l'héritage. Par exemple, l'égalité d'instance class Aet class B extends Apeut dépendre de détails concrets de l'application.
  • Idéalement, utilisez une clé d'entreprise comme ID

Attendant vos commentaires avec hâte.

aux
la source
0

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.

Christopher Yang
la source
0

OMI, vous avez 3 options pour implémenter equals / hashCode

  • Utiliser une identité générée par l'application, c'est-à-dire un UUID
  • L'implémenter sur la base d'une clé métier
  • L'implémenter en fonction de la clé primaire

L'utilisation d'une identité générée par l'application est l'approche la plus simple, mais présente quelques inconvénients

  • Les jointures sont plus lentes lors de son utilisation en tant que PK car 128 bits est tout simplement plus grand que 32 ou 64 bits
  • "Le débogage est plus difficile" car vérifier de ses propres yeux si certaines données sont correctes est assez difficile

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.

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

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.

  • Les jointures sont plus lentes car la jointure basée sur du texte de longueur variable est tout simplement lente. Certains SGBD peuvent même avoir des problèmes pour créer un index si la clé dépasse une certaine longueur.
  • D'après mon expérience, les clés métier ont tendance à changer, ce qui nécessitera des mises à jour en cascade des objets s'y référant. C'est impossible si des systèmes externes s'y réfèrent

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:

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

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:

  • Supprimer temporairement l'objet des collections basées sur le hachage
  • Persistez
  • Ajouter à nouveau l'objet aux collections basées sur le hachage

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.

Christian Beikov
la source
0

Avec le nouveau style de instanceoffrom java 14, vous pouvez implémenter le equalsen une seule ligne.

@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;
}
Artem
la source
-1

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:

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

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?

illEatYourPuppies
la source
Une approche qui fonctionne très bien si vous êtes prêt à tirer le meilleur parti des performances en générant le guide lors d'une recherche de base de données.
Michael Wiles
1
Qu'en est-il des tests unitaires de voiture? Dans ce cas, vous avez besoin d'une connexion à la base de données pour les tests? De plus, vos objets de domaine ne doivent pas dépendre de la persistance.
jhegedus
-1

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:

  • equals () signifie "l'objet fait référence à la même entité logique"
  • utiliser autant que possible l'ID de la base de données car pourquoi ferais-je le travail deux fois (souci de performances)
  • éviter le problème lors de l'accès à hashCode () / equals () sur une entité pas encore persistante et conserver le même comportement après qu'il soit en effet persistant

J'apprécierais vraiment les commentaires sur ma solution mixte ci-dessous

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;
    }
user2083808
la source
que voulez-vous dire par "UUID généré un jour sur cette entité avant que je ne persiste"? pourriez-vous donner un exemple pour ce cas?
jhegedus
pourriez-vous utiliser le type de génération attribué? Pourquoi le type de génération d'identité est-il nécessaire? a-t-il un avantage sur l'attribution?
jhegedus
que se passe-t-il si vous 1) créez une nouvelle MyEntity, 2) mettez-la dans une liste, 3) puis enregistrez-la dans la base de données puis 4) vous chargez cette entité de la base de données et 5) essayez de voir si l'instance chargée est dans la liste . Je suppose que ce ne sera pas le cas, même s'il devrait l'être.
jhegedus
Merci pour vos premiers commentaires qui m'ont montré que je n'étais pas aussi clair que je le devrais. Premièrement, "UUID généré un jour sur cette entité avant que je ne persiste" était une faute de frappe ... "avant que les TI persistent" aurait dû être lu à la place. Pour les autres remarques, je vais bientôt éditer mon post pour essayer de mieux expliquer ma solution.
user2083808
-1

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

Juste conscient de la manipulation de nouveaux objets dans HashSet / HashMap. En revanche, l'option 1 (rester Objectmise en œuvre) est rompue juste après merge, 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 hashCodepar constante, comme indiqué ci-dessous, Vlad Mihalcea a été conseillé.

Grigory Kislin
la source
-2

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:

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()
}
jhegedus
la source