380 votes

Le JPA hashCode () / equals () dilemme

Il y a eu quelques discussions ici sur des entités JPA et qui hashCode()/equals() mise en œuvre devrait être utilisé pour les classes d'entité JPA. La plupart (si pas tous) d'entre eux dépendent de mise en veille prolongée, mais j'aimerais en discuter JPA-mise en œuvre-neutre (je suis en utilisant EclipseLink, par la voie).

Toutes les implémentations possibles sont d'avoir leurs propres avantages et inconvénients concernant:

  • hashCode()/equals() contrat de la conformité (immuabilité) List/Set opérations
  • Si l' identique des objets (par exemple à partir de différentes sessions, des proxies dynamiques de paresseusement-chargé de structures de données) peuvent être détectés
  • Si les entités se comportent correctement dans détaché (ou non persistantes) état

Aussi loin que je peux voir, il y a trois options:

  1. Ne pas les remplacer; s'appuyer sur Object.equals() et Object.hashCode()
    • hashCode()/equals() de travail
    • impossible d'identifier des objets identiques, les problèmes avec des proxies dynamiques
    • pas de problèmes avec du recul des entités
  2. Remplacer, à partir de la clé primaire
    • hashCode()/equals() sont cassés
    • identité correcte (pour toutes les entités gérées)
    • problèmes avec du recul des entités
  3. De les remplacer, basée sur le Business-Id (non champs de clé primaire; ce sur les clés étrangères?)
    • hashCode()/equals() sont cassés
    • identité correcte (pour toutes les entités gérées)
    • pas de problèmes avec du recul des entités

Mes questions sont les suivantes:

  1. Ai-je raté une option et/ou pro/con?
  2. Quelle option avez-vous choisi et pourquoi?



Mise à JOUR 1:

Par "hashCode()/equals() sont cassé", je veux dire qu'successives hashCode() invocations peuvent renvoyer des valeurs différentes, ce qui est (lorsqu'il est correctement mis en œuvre) ne se décompose pas dans le sens de l' Object documentation de l'API, mais qui pose des problèmes lorsque vous essayez de récupérer un changement de l'entité à partir d'un Map, Set ou à d'autres à base de hachage Collection. Par conséquent, les implémentations JPA (au moins EclipseLink) ne fonctionnera pas correctement dans certains cas.

Mise à JOUR 2:

Merci pour vos réponses -- la plupart d'entre eux ont une qualité remarquable.
Malheureusement, je suis toujours pas sûr de l'approche qui sera le meilleur pour une vraie vie de l'application, ou la façon de déterminer la meilleure approche pour mon application. Donc, je vais garder ouverte la question et d'espoir pour certains plus de discussions et/ou des opinions.

148voto

Stijn Geukens Points 5482

Lire ce très bel article sur le sujet: Ne pas Laisser Hibernate Voler Votre Identité.

La conclusion de l'article qui va comme ceci:

L'identité de l'objet est trompeusement difficile à mettre en œuvre correctement quand les objets sont conservées dans une base de données. Cependant, les problèmes de la tige entièrement de permettre à des objets d'exister sans une carte d'identité avant qu'ils sont sauvé. Nous pouvons résoudre ces problèmes en prenant la responsabilité de affectation d'Id d'objet à l'écart de mapping objet-relationnel cadres telles que la mise en veille. Au lieu de cela, les Id d'objet peut être attribué dès que l' l'objet est instancié. Cela rend l'identité de l'objet simple et erreur, et réduit la quantité de code nécessaire dans le modèle de domaine.

68voto

nanda Points 12764

J'ai toujours la priorité est égale à/hashcode et le mettre en œuvre en fonction de l'id. Semble la solution la plus raisonnable pour moi. Voir la suite le lien.

Pour résumer tout ça, voici une liste de ce qui fonctionne ou ne fonctionne pas avec les différentes façons de le gérer est égal à/hashCode: enter image description here

EDIT:

Pour expliquer pourquoi cela fonctionne pour moi:

  1. Je n'utilisez pas habituellement haché-collection (HashMap/HashSet) dans mon application JPA. Si je dois, je préfère créer UniqueList solution.
  2. Je pense que changer d'affaires de l'id 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 la suppression de l'élément et de le remettre à la hachée collection.
  3. Pour mon modèle, j'ai mis de l'entreprise id sur le constructeur et ne fournit pas de poseurs. Je laisse implémentation JPA pour modifier le champ au lieu de la propriété.
  4. L'UUID de la solution semble être exagéré. Pourquoi UUID si vous avez naturelle d'affaires de l'id? Je voudrais, après tous ensemble le caractère unique de l'entreprise id dans la base de données. Pourquoi avoir TROIS indices pour chaque table de la base de données?

42voto

lweller Points 5252

Personnellement, j'ai déjà utilisé l'une de ces trois stratégies dans les différents projets. Je dois dire que l'option 1 est à mon avis le plus possible dans la vie réelle application. Faites l'expérience de la rupture hashCode () et equals() de la conformité conduit à de nombreuses fou bugs que vous aurez à chaque fois retrouver dans des situations où le résultat de l'égalité des changements après qu'une entité a été ajouté à une collection.

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


a) hashCode/égale basée sur un ensemble d' immuable, not null, constructeur attribué, les champs

(+) les trois critères sont garantis

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

(-) compliquent la manipulation si vous devez changer un puis


b) hashCode/égale basée sur la clé primaire qui est affectée par l'application (dans le constructeur) au lieu de JPA

(+) les trois critères sont garantis

(-) vous ne pouvez pas profiter de simple et fiable de génération d'ID de stratégies comme DB séquences

(-) si compliqué de nouvelles entités sont créées dans un environnement distribué (client/serveur) ou de l'application de serveur de cluster


c) hashCode/égale basé sur un UUID attribué par le constructeur de l'entité

(+) les trois critères sont garantis

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

(-) peut-être un peu plus de risque que deux fois le même UUID est utilisé, en fonction de l'algorithme utilisé (peut être détecté par un index unique sur DB)

36voto

Nous avons habituellement deux Id dans nos entités:

  1. Est pour la couche de persistance (que fournisseur de persistance et de la base de données peuvent comprendre les relations entre les objets).
  2. Est pour nos besoins de l'application (equals() et hashCode() en particulier)

Prendre un coup d'oeil:

@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 de vue concernant les appels d' 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("yoda@jedicouncil.org");

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

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

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

Quand je lance mes tests et voir le journal de sortie-je résoudre le problème:

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

Alternativement, on peut distinguer constructeur:

@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 pourrait ressembler à 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 deux constructeurs approche plus approprié pour vous.

36voto

Chris Lercher Points 22134

Si vous souhaitez utiliser equals()/hashCode() vos Jeux, en ce sens que la même entité ne peut être là qu'une fois, alors il n'y a qu'une seule option: l'Option 2. C'est parce que d'une clé primaire pour une entité qui, par définition, ne change jamais (si quelqu'un, en effet, mises à jour, ce n'est pas la même entité plus)

Vous devriez prendre littéralement: Depuis votre equals()/hashCode() sont basés sur la clé primaire, vous ne devez pas utiliser ces méthodes, jusqu'à ce que la clé primaire est définie. Donc, vous ne devez pas mettre des entités dans l'ensemble, jusqu'à ce qu'ils sont affectés à une clé primaire. (Oui, Uuid et des concepts similaires peut aider à attribuer les clés primaires tôt.)

Maintenant, il est aussi théoriquement possible d'atteindre qu'avec l'Option 3, même si les soi-disant "business-clés" ont le désagréable inconvénient qu'ils peuvent changer: "Tout ce que vous aurez à faire est de supprimer le déjà inséré des entités de l'ensemble(s), et insérez-la de nouveau." C'est vrai - mais cela signifie aussi, que dans un système distribué, vous aurez à vous assurer que cela est fait, absolument partout, les données ont été insérées à (et vous aurez à vous assurer, que la mise à jour est effectuée, avant que d'autres choses se produisent). Vous aurez besoin d'un système sophistiqué de mécanisme de mise à jour, surtout si certains systèmes à distance ne sont pas joignable...

L'Option 1 ne peut être utilisée que si tous les objets dans vos jeux sont de la même session Hibernate. La documentation Hibernate très clair à ce sujet dans le chapitre 13.1.3. Considérant l'identité de l'objet:

Au sein d'une Session de l'application peut utiliser en toute sécurité == pour comparer des objets.

Toutefois, une application qui utilise == en dehors d'une Session peut produire des résultats inattendus. Cela peut se produire même dans certains des endroits inattendus. Par exemple, si vous mettez deux instances détachées dans le même Ensemble, les deux ont la même identité de base de données (c'est à dire, qu'ils représentent la même ligne). JVM identité, cependant, est, par définition, pas garanti dans les cas d'un état détaché. Le développeur a remplacer la equals() et hashCode() méthodes dans les classes persistantes et de mettre en œuvre leur propre notion de l'objet de l'égalité.

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

Il y a une mise en garde: ne jamais utiliser l'identificateur de base de données pour mettre en œuvre l'égalité. Utiliser une clé est une combinaison unique, généralement immuable, les attributs. L'identificateur de base de données va changer, si un transitoire objet est persistant. Si l'instance éphémère (généralement en collaboration avec des instances détachées) est maintenu dans un Ensemble, la modification de la hashcode 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 encore 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.

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

Attributs pour les affaires clés n'ont pas à être aussi stable que la base de données de clés primaires; vous n'avez qu'à garantir la stabilité, tant que les objets sont dans le même Ensemble.

Cela est correct. Le problème pratique que je vois c'est: Si vous ne pouvez pas la garantie absolue de la stabilité, comment allez-vous être en mesure de garantir la stabilité "tant que les objets sont dans le même Ensemble". Je peux imaginer certains cas particuliers (comme l'utilisation de jeux uniquement pour une conversation, puis de le jeter), mais j'ai de la question générale de la faisabilité de cette.


Version courte:

  • Option 1 peut être utilisé uniquement avec les objets à l'intérieur d'une même session.
  • Si vous le pouvez, utilisez l'Option 2. (Attribuer PK le plus tôt possible, parce que vous ne pouvez pas utiliser les objets dans des ensembles jusqu'à ce que le PK est attribué.)
  • Si vous pouvez garantir la stabilité relative, vous pouvez utiliser l'Option 3. Mais être prudent avec ce.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X