web-dev-qa-db-fra.com

Le dilemme JPA hashCode ()/equals ()

Il y a eu quelquesdiscussions ici sur les entités JPA et sur l'implémentation de hashCode()/equals() 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 avec JPA-implementation (j'utilise EclipseLink, au fait).

Toutes les implémentations possibles ont leurs propres avantages et inconvénients concernant:

  • hashCode()/equals() contrat conformité (immuabilité) pour les opérations List/Set
  • Si les objets identiques (par exemple, issus de sessions différentes, de mandataires dynamiques issus de structures de données chargées paresseusement) peuvent être détectés
  • Si les entités se comportent correctement dans état détaché (ou non persistant)

Autant que je sache, il y a trois options:

  1. Ne les remplacez pas; compter sur Object.equals() et Object.hashCode()
    • hashCode()/equals() work
    • ne peut pas identifier les objets identiques, problèmes avec les mandataires dynamiques
    • pas de problèmes 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 du Business-Id (champs de clé non principale; qu'en est-il des clés étrangères?)
    • hashCode()/equals() sont cassés
    • identité correcte (pour toutes les entités gérées)
    • pas de problèmes avec les entités détachées

_ {Mes questions sont:} _

  1. Ai-je raté une option et/ou un point favorable/négatif?
  2. Quelle option as-tu choisi et pourquoi?



UPDATE 1:

Par "hashCode()/equals() sont cassés", je veux dire que des invocations hashCode() successives peuvent renvoyer des valeurs différentes, ce qui (lorsqu'il est correctement implémenté) n'est pas rompu au sens de la documentation de l'API Object, mais pose des problèmes lors de la tentative de récupération d'une entité modifiée Map, Set ou une autre Collection basée sur le hachage. Par conséquent, les implémentations JPA (au moins EclipseLink) ne fonctionneront pas correctement dans certains cas.

UPDATE 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, ni comment déterminer la meilleure approche pour mon application. Je vais donc garder la question ouverte et espérer d'autres discussions et/ou opinions.

288
MRalwasser

Lisez cet article très agréable sur le sujet: Ne laissez pas Hibernate voler votre identité .

La conclusion de l'article est la suivante:

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

101
Stijn Geukens

J'ai toujours priorité sur equals/hashcode et je l'implémente en fonction de l'identifiant de l'entreprise. Semble la solution la plus raisonnable pour moi. Voir le lien suivant link .

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: enter image description here

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 la solution UniqueList.
  2. Je pense que le changement d'identifiant d'entreprise au moment de l'exécution n'est pas une bonne pratique pour les applications de base de données. Dans les rares cas où il n'y a pas d'autre solution, je ferais un traitement spécial, comme enlever l'élément et le remettre à la collection basée sur le hachage.
  3. Pour mon modèle, j'ai défini l'ID d'entreprise sur le constructeur et je ne lui ai pas fourni de paramètres. Je laisse l'implémentation JPA changer le field au lieu de la propriété.
  4. La solution UUID semble exagérée. Pourquoi UUID si vous avez un identifiant d'entreprise 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?
62
nanda

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

  1. S'applique uniquement à la couche de persistance (afin que le fournisseur de persistance et la base de données puissent comprendre 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 méthode setUuid(). 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 je lance mes tests et que je vois la sortie du journal, je résous le problème:

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

Alternativement, on peut fournir un constructeur séparé:

@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 configurateur, mais vous pouvez trouver une approche à deux constructeurs plus appropriée.

Si vous souhaitez utiliser equals()/hashCode() pour vos ensembles, dans le sens où la même entité ne peut y figurer 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 la met effectivement à jour, ce n'est plus la même entité)

Vous devriez prendre cela à la lettre: puisque votre equals()/hashCode() est basé sur la clé primaire, vous ne devez pas utiliser ces méthodes tant que la clé primaire n'est pas définie. Donc, vous ne devriez pas mettre d'entités dans l'ensemble, jusqu'à ce qu'on leur attribue une clé primaire. (Oui, les UUID et des concepts similaires peuvent aider à attribuer des clés primaires à un stade précoce.)

Or, il est théoriquement également possible d’atteindre cet objectif avec l’option 3, même si les soi-disant "clés métier" présentent le désagréable inconvénient qu’elles peuvent changer: "Il ne vous reste plus qu'à supprimer les entités déjà insérées de l'ensemble ( s) et les réinsérer. " C’est vrai, mais cela signifie également que, dans un système distribué, vous devez vous assurer que cela est fait absolument partout où les données ont été insérées (et vous devez 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é, en particulier 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 l'explique très clairement au chapitre 13.1.3. Prise en compte de l'identité d'objet :

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

Cependant, une application utilisant == en dehors d'une session peut produire des résultats inattendus. Cela peut se produire même dans des 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). Par définition, l’identité de la machine virtuelle Java n’est par définition pas garantie pour les instances détachées. Le développeur doit redéfinir les méthodes equals () et hashCode () dans les classes persistantes et implémenter leur propre notion d'égalité d'objet.

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

Il y a une mise en garde: ne jamais utiliser l'identifiant de la base de données pour implémenter l'égalité. Utilisez une clé métier combinant des 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'identifiant à l'avance (par exemple en utilisant des UUID)
  • et pourtant vous voulez absolument mettre vos objets dans des ensembles alors qu'ils sont dans un état transitoire.

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

Ensuite, il est mentionné la nécessité d’une relative stabilité:

Les attributs des clés métier ne doivent pas nécessairement être aussi stables que les clés primaires de la base de données; vous devez seulement 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 le suivant: 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 (utiliser des décors uniquement pour une conversation, puis les jeter), mais je remets en question la faisabilité générale de cette situation.


Version courte:

  • L'option 1 ne peut être utilisée qu'avec des objets au sein d'une même session.
  • Si vous le pouvez, utilisez l'option 2. (Attribuez la PC le plus tôt possible, car vous ne pouvez pas utiliser les objets des ensembles tant que la PK n'est pas affectée.)
  • Si vous pouvez garantir une stabilité relative, vous pouvez utiliser l’option 3. Mais faites attention.
29
Chris Lercher

J'ai personnellement déjà utilisé ces trois stratégies dans différents projets. Je dois dire que l’option 1 est à mon avis la plus pratique dans une application réelle. Faire l'expérience de la rupture de la conformité à hashCode ()/equals () conduit à de nombreux bugs loufoques, comme vous le ferez chaque fois dans des situations où le résultat de l'égalité change après l'ajout d'une entité à une collection.

Mais il existe d'autres options (avec leurs avantages et leurs inconvénients):


a) hashCode/est égal à un ensemble de immuable, non nul, constructeur attribué, champs

(+) les trois critères sont garantis

(-) les valeurs de champ doivent être disponibles pour créer une nouvelle instance

(-) compliquer la manipulation si vous devez changer l’un d’eux


b) hashCode/est égal à la clé primaire assignée par application (dans le constructeur) au lieu de JPA

(+) les trois critères sont garantis

(-) vous ne pouvez pas tirer parti de stratégies simples et fiables de génération d'identifiants telles que 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/est égal à un UUID assigné par le constructeur de l'entité

(+) les trois critères sont garantis

(-) frais généraux de la génération d'UUID

(-) peut être un peu risqué d'utiliser deux fois le même UUID, en fonction de l'algorithme utilisé (peut être détecté par un index unique sur la base de données)

26
lweller

Bien que l'utilisation d'une clé métier (option 3) soit l'approche la plus couramment recommandée ( Hibernate community wiki , "Java Persistence with Hibernate" p. 398), et c'est ce que nous utilisons le plus souvent, il y a un bogue Hibernate qui rompt ce problème. pour les ensembles très recherchés: HHH-3799 . Dans ce cas, Hibernate peut ajouter une entité à un ensemble avant que ses champs ne soient initialisés. Je ne suis pas sûr de savoir pourquoi ce bogue n'a pas attiré plus d'attention, car cela rend vraiment problématique l'approche recommandée par la clé métier.

Je pense que le cœur du problème est qu'égaux et hashCode devraient ê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 a non telle immuable Etat. Hibernate modifie la clé primaire lorsqu'un objet transitoire devient persistant. Hibernate modifie également la clé commerciale 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 "Ne laissez pas Hibernate voler votre identité" (déjà référencé de la réponse de Stijn Geukens) et de Lance Arlaus dans "Génération d'objets: une meilleure approche de l'intégration d'Hibernate" .

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 d'égal à égal et hashCode laisse au développeur le soin de décider de la signification de 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 à cette fin, par exemple boolean sameEntity ou boolean dbEquivalent ou boolean businessEquals.

10
jbyler
  1. Si vous avez une clé business , vous devriez l’utiliser pour equals/hashCode.
  2. Si vous ne possédez pas de clé d'entreprise, vous ne devez pas la laisser avec les implémentations par défaut Object égal à et hashCode car cela ne fonctionne pas après merge et entité.
  3. Vous pouvez utiliser l'identifiant d'entité comme suggéré dans cet article . Le seul problème est que vous devez utiliser une implémentation hashCode qui retourne 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
    }
    
9
Vlad Mihalcea

Je suis d'accord avec la réponse d'Andrew. Nous faisons la même chose dans notre application, mais au lieu de stocker des UUID en tant que VARCHAR/CHAR, nous les scindons 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 vous intéresser à la génération lente de l’UUID uniquement lorsque cela est nécessaire, par exemple lors de la persistance ou des 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();
    }

}
5
Drew

Comme d'autres personnes plus intelligentes que moi l'ont déjà souligné, il existe de nombreuses stratégies. Il semble cependant que la majorité des modèles de conception appliqués tentent de se frayer un chemin vers le succès. Ils limitent l'accès des constructeurs s'ils ne gênent pas complètement les invocations des 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 substitutions d’égal à égal et de hashcode compatibles avec l’application, je me demande alors si ces stratégies sont conformes à KISS (Keep It Simple Stupid).

Pour moi, j'aime bien remplacer equals et hashcode en examinant l'identifiant. Dans ces méthodes, j'exige 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 ne respecte pas ce contrat échouera dans la minute (espérons-le).

Attention cependant: si vos entités sont stockées dans différentes tables 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 pour tous les types d’entités. Dans ce cas, comparez également les types d'exécution avec un appel à Object # getClass () , ce qui empêchera bien entendu que deux types différents soient considérés comme égaux. Cela me va très bien pour la plupart.

3

Il y a évidemment déjà des réponses très instructives, mais je vais vous dire ce que nous faisons.

Nous ne faisons rien (c.-à-d. Ne pas remplacer).

Si nous avons besoin d’equals/hashcode pour les collections, nous utilisons des UUID . Vous créez simplement l’UUID dans le constructeur. Nous utilisons http://wiki.fasterxml.com/JugHome pour UUID. L'UUID est un peu plus coûteux en termes de ressources processeur, mais est peu coûteux par rapport à la sérialisation et à l'accès à la base de données.

2
Adam Gent

L'approche des clés commerciales ne nous convient pas. Nous utilisons la base de données généréeID, transitoire temporaire tempId et override equal ()/hashcode () pour résoudre le dilemme. Toutes les entités sont des descendants de l'entité. Avantages:

  1. Pas de champs supplémentaires dans la base de données
  2. Pas de codage supplémentaire dans les entités de descendants, une approche pour tous
  3. Aucun problème de performances (comme avec UUID), génération de DB Id
  4. Aucun problème avec Hashmaps (inutile de garder à l'esprit l'utilisation de equal & etc)
  5. Le hashcode de la nouvelle entité ne change pas dans le temps, même après persistance

Les inconvénients:

  1. Il peut y avoir des problèmes avec la sérialisation et la désérialisation d'entités non persistantes
  2. Le hashcode de l'entité enregistrée peut changer après le rechargement depuis la base de données
  3. Objets non persistants considérés comme toujours différents (c'est peut-être 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();
    }
}
1
Demel

Veuillez considérer l'approche suivante en fonction de l'identificateur de type prédéfini et de l'ID.

Les hypothèses spécifiques pour JPA:

  • les entités du même "type" et le même identifiant non nul sont considérés comme égaux
  • les entités non persistantes (en supposant qu'il n'y ait pas d'identifiant) 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é
  • s'assure que les sous-classes fournissent l'identité de type
  • comportement prédit avec les classes proxy

Désavantages:

  • Requiert que chaque entité appelle super()

Remarques:

  • Besoin d'attention lors de l'utilisation de l'héritage. Par exemple. L’égalité d’instance entre class A et class B extends A peut dépendre de détails concrets de l’application.
  • Idéalement, utilisez une clé commerciale comme identifiant

Attendant vos commentaires avec hâte.

1
aux

Il s'agit d'un problème courant dans tous les systèmes informatiques utilisant Java et JPA. Le point douloureux va au-delà de la mise en œuvre de equals () et hashCode (), il affecte la façon dont une organisation fait référence à une entité et la façon dont ses clients font référence à la même entité. J'ai vu assez de peine de ne pas avoir de clé d'entreprise au point que j'ai écrit mon propre blog pour exprimer mon point de vue.

En bref: utilisez un ID court, lisible par un humain, séquentiel avec des préfixes significatifs comme clé commerciale, généré sans aucune dépendance sur un stockage autre que la RAM. Twitter's Snowflake est un très bon exemple.

0
Christopher Yang

J'ai toujours utilisé l'option 1 par le passé car j'étais au courant de ces discussions et pensais qu'il était préférable de ne rien faire jusqu'à ce que je sache ce qu'il convient de faire. Ces systèmes fonctionnent toujours avec succès.

Cependant, la prochaine fois, je pourrai essayer l'option 2 - en utilisant l'ID généré par la base de données.

Hashcode et equals lanceront IllegalStateException si l'id n'est pas défini.

Cela empêchera les erreurs subtiles impliquant des entités non enregistrées d'apparaître de manière inattendue.

Que pensent les gens de cette approche?

0
Neil Stevens

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

  • Utiliser une identité générée par une application, par exemple un UUID
  • Implémentez-le en fonction d'une clé métier
  • Implémentez-le en fonction de la clé primaire

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

  • Les jointures sont plus lentes si vous l'utilisez 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 vos propres yeux si certaines données sont correctes est assez difficile

Si vous pouvez travailler avec ces inconvénients, utilisez simplement cette approche.

Pour résoudre le problème de la jointure, vous pouvez utiliser l'UUID en tant que clé naturelle et une valeur de séquence en tant que clé primaire, mais vous pouvez toujours rencontrer les problèmes d'implémentation d'égaux/hashCode dans les entités enfant compositionnelles comportant un identifiant incorporé, car vous souhaiterez une jointure. sur la clé primaire. L'utilisation de la clé naturelle dans les entités enfants id et de la clé primaire pour faire référence au parent constitue 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 évite tous les inconvénients et vous fournit en même temps une valeur (l’UUID) que vous pouvez partager avec des systèmes externes sans exposer les éléments internes de ceux-ci.

Implémentez-le à l'aide d'une clé métier si vous pouvez vous attendre à ce que l'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 les jointures basées sur un texte de longueur variable sont tout simplement lentes. 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écessite des mises à jour en cascade des objets qui y font référence. Ceci est impossible si des systèmes externes s'y réfèrent

OMI, vous ne devez pas implémenter ou travailler avec une clé métier exclusivement. C’est un bon complément, c’est-à-dire que les utilisateurs peuvent rechercher rapidement avec cette clé d’entreprise, mais le système ne devrait pas en dépendre pour son fonctionnement.

Implémenter en fonction de la clé primaire a ses problèmes, mais peut-être que ce n'est 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 vous n'êtes pas obligé de . Le problème d'utilisation d'un identifiant généré par un SGBD dans equals/hashCode provient du fait que l'objet a peut-être été ajouté à hash. collections avant d'attribuer l'identifiant.

La solution évidente consiste à ne pas simplement ajouter l'objet à des collections basées sur un hachage avant d'attribuer l'identifiant. Je comprends que ce n’est pas toujours possible, car vous souhaiterez peut-être une déduplication avant d’attribuer déjà l’ID. Pour pouvoir continuer à utiliser les collections basées sur le hachage, il vous suffit de reconstruire les collections après avoir attribué l'identifiant.

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 suis pas sûr de savoir comment changer les collections lors d'événements pré et post-persistants fonctionne, mais l'idée est la suivante:

  • Supprimer temporairement l'objet des collections à base de hachage
  • Le persister
  • Ré-ajouter l'objet aux collections basées sur le hachage

Une autre solution consiste à reconstruire simplement tous vos modèles basés sur le hachage après une mise à jour/persistante.

En fin de compte, c'est à vous de décider. Personnellement, j'utilise l'approche basée sur les séquences la plupart du temps et j'utilise l'approche UUID uniquement si j'ai besoin d'exposer un identifiant à des systèmes externes.

0
Christian Beikov