Le dilemme du hashCode() / equals ()

Il y a eu des certains "1519240920 des" discussions ici, à propos des entités JPA et qui hashCode() / equals() la mise en œuvre devrait être utilisé pour les classes d'entité JPA. La plupart (si pas tous) d'entre eux dépendent de L'hibernation, mais je voudrais les discuter JPA-implémentation-de manière neutre (J'utilise EclipseLink, d'ailleurs).

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

  • hashCode() / equals() contrat conformité (immutabilité) pour List / Set opérations
  • si objets identiques (par exemple, provenant de différentes sessions, mandataires dynamiques 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-persisté)

autant que je puisse voir, Il ya trois options :

  1. ne les annulent pas; s'appuient sur Object.equals() et Object.hashCode()
    • hashCode() / equals() emploi 1519390920"
    • ne peut pas identifier des objets identiques, problèmes avec des approximations dynamiques
    • pas de problèmes avec du recul des entités
  2. les outrepasser, basé sur 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. les outrepasser, basé sur le Business-Id (champs clés non primaires; 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 du recul des entités

mes questions sont:

  1. ai-je manqué une option et/ou un point pro/con?

  2. quelle option avez-vous choisie et pourquoi?





Maj 1:

By hashCode() / equals() are broken", je veux dire que les invocations successives de hashCode() peuvent retourner des valeurs différentes, ce qui est (lorsqu'il est correctement mis en œuvre) non cassé dans le sens de la documentation de L'API Object , mais qui cause des problèmes lors de la recherche d'un changement d'entité d'un Map , Set ou autre Collection basé sur le hash . 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 d'entre elles ont une qualité remarquable.

Malheureusement, je ne suis toujours pas certain de l'approche qui sera la meilleure pour une application réelle, ou comment déterminer la la meilleure approche pour ma candidature. Donc, je vais garder la question ouverte et j'espère que d'autres discussions et/ou opinions.

262
demandé sur Community 2011-02-17 19:22:13

18 réponses

lisez cet article très agréable sur le sujet: ne laissez pas L'hibernation vous voler votre identité .

la conclusion de l'article 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 aux objets d'exister sans id avant qu'ils ne soient enregistrer. Nous pouvons résoudre ces problèmes en prenant le la responsabilité de l' attribuer des ID d'objet loin des cadres de mappage relatifs aux objets telles que la mise en veille. Au lieu de cela, les identificateurs d'objet peuvent être assignés dès que le 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.

91
répondu Stijn Geukens 2018-06-14 22:53:11

nous avons habituellement deux identifiants dans nos entités:

  1. s'applique uniquement à la couche de persistance (de sorte que le fournisseur de persistance et la base de données peuvent établir des relations entre les objets).
  2. est pour nos besoins d'application ( equals() et hashCode() en particulier)

regardez:

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

modifier: 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("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 j'exécute mes tests et que je vois la sortie du journal, je règle 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 setter, mais vous pouvez trouver deux constructeurs approche plus approprié pour vous.

31
répondu Andrew Андрей Листочкин 2012-04-17 21:23:35

si vous voulez utiliser equals()/hashCode() pour vos ensembles, dans le sens où la même entité ne peut être là qu'une seule fois, alors il n'y a qu'une seule option: L'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 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 devriez pas mettre des entités dans le plateau, jusqu'à ce qu'on leur assigne une clé primaire. (Oui, UUIDs et des concepts similaires peuvent aider à assigner des clés primaires tôt.)

maintenant, il est théoriquement aussi possible d'atteindre cet objectif avec L'Option 3, même si les soi-disant "business-keys" ont l'inconvénient désagréable qu'ils peuvent changer: "tout ce que vous aurez à faire est de supprimer les entités déjà insérées à partir de l'ensemble(s), et de les réinsérer."C'est vrai - mais cela signifie aussi, que dans un système distribué, vous devrez vous assurer, que cela est 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 D'hibernation. La documentation D'hibernation 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 pourrait produire des résultats inattendus. Cela peut se produire même dans certains endroits inattendus. Par exemple, si vous mettez deux instances détachées dans le même ensemble, les deux peuvent avoir la même identité de base de données (i.e., ils représentent même ligne). L'identité de la JVM n'est toutefois pas, par définition, garantie dans les cas de détachement. Le développeur doit outrepasser les méthodes equals() et hashCode() dans les classes persistantes et mettre en œuvre 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'Identificateur de la base de données pour mettre en œuvre l'égalité. Utiliser une clé d'entreprise qui est une combinaison de attributs uniques, généralement immuables. L'identifiant de la base de données sera modifié si un objet transitoire est rendu persistant. Si l'instance transitoire (généralement avec les instances détachées) est tenue dans un ensemble, changer le hashcode brise le contrat de l'ensemble.

C'est vrai, si vous

  • ne peut pas attribuer l'id en avance (p.ex. en utilisant UUIDs)
  • et pourtant vous voulez absolument mettez vos objets en séries alors qu'ils sont dans un état transitoire.

sinon, vous êtes libre de choisir L'Option 2.

puis il mentionne le besoin d'une stabilité relative:

les attributs pour clés business ne doivent pas être aussi stables que les clés primaires de la base de données; vous devez seulement garantir la stabilité aussi longtemps que les objets sont dans le même ensemble.

c'est correct. Le le problème pratique que je vois avec ceci est: si vous ne pouvez pas garantir une stabilité absolue, comment serez-vous en mesure de garantir la stabilité "tant que les objets sont dans le même ensemble". Je peux imaginer quelques cas spéciaux (comme utiliser des ensembles seulement pour une conversation et ensuite jeter), mais je voudrais remettre en question la praticabilité générale de ceci.


version Courte:

  • Option 1 ne peut être utilisé qu'avec des objets à l'intérieur d'un seul 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 une stabilité relative, Vous pouvez utiliser L'Option 3. Mais fais attention avec ça.
26
répondu Chris Lercher 2011-02-25 10:11:36

personnellement, j'ai déjà utilisé toutes ces trois stratégies dans différents projets. Je dois dire que l'option 1 est à mon avis le plus possible dans la vie réelle application. A fait l'expérience de briser la conformité hashCode()/equals() conduit à de nombreux bogues fous comme vous finirez chaque fois dans des situations où le résultat des changements d'égalité après une entité a été ajoutée à une collection.

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


) hashCode/égale basé sur un ensemble de immuable , not null , constructeur attribué , les champs

( + ) les trois critères sont garantis

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

( - ) compliquer la manipulation si vous devez changer l'un des Puis


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

( + ) les trois critères sont garantis

( - ) vous ne pouvez pas profiter de stratégies de génération D'ID fiables simples comme les séquences de DB

( - ) compliqué si de nouvelles entités sont créées dans un environnement distribué (client/serveur) ou un groupe de serveurs app


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

( + ) les trois critères sont garantis

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

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

25
répondu lweller 2011-02-17 16:58:32

Although using a business key (option 3) is the most commonly recommended approach ( Hibernate community wiki ," Java Persistence with Hibernate " p. 398), et c'est ce que nous utilisons le plus souvent, il y a un bug D'hibernation qui casse ce pour des ensembles impatients: HHH-3799 . Dans ce cas, Hibernate peut ajouter une entité à un ensemble avant que ses champs sont initialisés. Je ne suis pas sûr pourquoi ce bug n'a pas obtenu plus d'attention, car il fait vraiment le activités recommandées-approche clé problématique.

je pense que le coeur de la question est que equals et hashCode devrait être fondée sur l'état immuable (référence Odersky et coll. ), et une entité Hibernée avec une clé primaire gérée par hibernation a Non un tel état immuable. La clé primaire est modifiée par hibernation lorsqu'un objet transitoire devient persistant. La clé d'entreprise est également modifiée par Hibernate, quand il hydrate un objet dans le processus d'être initialisé.

qui ne laisse que l'option 1, héritant du java.lang.Implémentations objet basées sur l'identité de l'objet, ou utilisant une clé primaire gérée par 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 avec les instances persistantes en utilisant .égal.)( Mais ce N'est pas grave; le contrat d'égaux et le hashCode laissent au développeur le soin de décider ce que l'égalité signifie pour chaque classe. Alors laissez les égaux et le hashCode hériter de L'objet. Si vous avez besoin de comparer une instance détachée à une instance persistante, vous pouvez créer une nouvelle méthode explicitement dans ce but, peut-être boolean sameEntity ou boolean dbEquivalent ou boolean businessEquals .

10
répondu jbyler 2013-09-13 01:22:57
  1. si vous avez une business key , vous devez utiliser cette option pour equals / hashCode .
  2. si vous n'avez pas de clé d'entreprise, vous ne devriez pas la laisser avec les implémentations par défaut Object égales et hashCode parce que cela ne fonctionne pas après vous merge et entité.
  3. Vous pouvez utiliser l'identifiant d'entité comme il est suggéré dans ce post . Le seul hic, est-ce que vous devez utiliser une implémentation hashCode 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
    }
    
7
répondu Vlad Mihalcea 2018-01-04 12:12:17

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 UUIDs comme VARCHAR/CHAR, nous les divisons en deux longues valeurs. Voir l'UUID.getLeastSignificantBits () et UUID.getMostSignificantBits ().

une autre chose à considérer, c'est que ça appelle L'UUID.randomUUID () sont assez lents, donc vous pourriez vouloir regarder dans la génération paresseuse de L'UUID seulement quand c'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();
    }

}
5
répondu Drew 2011-02-26 21:56:55

comme d'autres personnes beaucoup plus intelligentes que moi l'ont déjà souligné, il y a un grand nombre de stratégies là-bas. Il semble être le cas cependant que la majorité des modèles de conception appliquée tentent de hacker leur chemin vers le succès. Ils limitent l'accès du constructeur, sinon entravent complètement les invocations du constructeur 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 faire les égaux - et les hashcodes l'emportent compatible avec l'application, alors je me demande si ces stratégies sont en conformité avec KISS (Keep It Simple Stupid).

pour moi, j'aime passer outre les égaux et le hashcode en examinant l'id. Dans ces méthodes, j'exige que l'id ne soit pas null et documente bien ce comportement. Ainsi, il deviendra le contrat de développeurs de persister une nouvelle entité avant de le stocker ailleurs. Une demande qui n'honore pas ce contrat échouerait dans la minute (espérons).

mise en garde: 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 de la clé primaire, vous obtiendrez des clés primaires dupliquées pour tous les types d'entités. Dans un tel cas, comparez aussi les types run time avec un appel à Object#getClass() ce qui bien sûr rendra impossible que deux types différents soient considérés égaux. Qui me convient très bien pour la plupart.

3
répondu Martin Andersson 2013-05-05 08:30:11

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

nous ne faisons rien (c.-à-d. ne pas outrepasser).

si nous avons besoin d'equals/hashcode pour travailler pour les collections, nous utilisons UUIDs. Vous créez juste L'UUID dans le constructeur. Nous utilisons http://wiki.fasterxml.com/JugHome pour UUID. UUID est un peu plus cher CPU sage mais est bon marché par rapport à la sérialisation et l'accès db.

2
répondu Adam Gent 2011-02-21 16:17:08

les clés d'Entreprise approche ne convient pas pour nous. Nous utilisons DB généré ID , temporaire, transitoire tempId et remplacer l'égalité () et hashcode() pour résoudre le dilemme. Toutes les entités sont des descendants de L'entité. Points forts:

  1. Pas de champs supplémentaires dans la bd
  2. Pas de codage supplémentaire dans descendants des entités, une approche pour tous
  3. aucun problème de performance (comme avec UUID), DB Id generation
  4. Pas de problème avec Hashmaps (n'avez pas besoin de garder à l'esprit l'utilisation de l'égalité et etc.)
  5. Hashcode de la nouvelle entité n'a pas changé dans le temps même après persisting

Inconvénients:

  1. Il y a peut être des problèmes avec la sérialisation et la désérialisation pas persisté entités
  2. Le Hashcode
  3. de l'entité sauvegardée peut changer après avoir été rechargé à partir de DB
  4. n'a pas persisté les objets considérés comme toujours différents (peut-être est-ce juste?)
  5. 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
répondu Demel 2015-04-10 13:20:07

veuillez considérer l'approche suivante basée sur l'Identificateur de type prédéfini et L'ID.

Les hypothèses de 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 l'absence de pièce d'identité) 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;
  }
}

Béton exemple d'entité:

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

exemple D'essai:

@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 une identité de type
  • , prédit le comportement avec les proxy classes

désavantages:

  • exige que chaque entité appelle super()

Notes:

  • nécessite une attention lors de l'utilisation de l'héritage. Par exemple: l'égalité d'instance de class A et class B extends A peut dépendre des détails concrets de la demande.
  • idéalement, utilisez une clé d'affaires comme ID

J'attends vos commentaires avec impatience.

1
répondu aux 2018-04-01 18:54:44

j'ai toujours utilisé l'option 1 dans le passé parce que j'étais au courant de ces discussions et j'ai pensé qu'il était préférable de 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 vont lancer IllegalStateException si l'id n'est pas défini.

cela évitera les erreurs subtiles impliquant les entités non sauvées de paraître inopinément.

que pensent les gens de cette approche?

0
répondu Neil Stevens 2013-08-30 21:30:52

C'est un problème courant dans tous les systèmes informatiques qui utilisent Java et JPA. Le point de la douleur s'étend au-delà de la mise en œuvre 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 une clé d'affaires au point que j'ai écrit mon propre blog pour exprimer mon point de vue.

en bref: utilisez un ID court, lisible par l'homme, séquentiel avec des préfixes significatifs en tant qu'entreprise clé générée sans aucune dépendance sur un stockage autre que la mémoire vive. Le flocon de neige de Twitter en est un très bon exemple.

0
répondu Christopher Yang 2014-10-15 17:49:31

si UUID est la réponse pour beaucoup de gens, pourquoi ne pas utiliser les méthodes d'usine de la couche business pour créer les entités et assigner 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é de la part du fournisseur de persistance, et nos fonctions hashCode() et equals() pourraient s'y fier.

nous pourrions également déclarer les constructeurs de la voiture protégés et ensuite, utilisez la réflexion dans notre méthode d'affaires pour y accéder. De cette façon les développeurs ne seraient pas l'intention sur instanciate voiture avec nouveau, mais par la méthode d'usine.

comment ça?

-1
répondu illEatYourPuppies 2012-06-15 13:50:22

j'ai essayé de répondre à cette question moi-même et n'a jamais été totalement heureux avec des solutions trouvées jusqu'à ce que je lis ce post et en particulier a tiré un. J'ai aimé la façon dont il paresseux créé UUID et stocké de façon optimale.

mais je voulais ajouter encore plus de flexibilité, c'est-à-dire créer UUID paresseux seulement quand hashCode()/equals() est accédé avant la première persistance de l'entité avec les avantages de chaque solution:

  • égal à () signifie " l'objet se rapporte à même entité logique"
  • utiliser L'ID de la base de données autant que possible parce que pourquoi ferais-je le travail deux fois (Problème de rendement)
  • prévenir le problème en accédant au hashCode () / equals () sur l'entité non encore persistante et conserver le même comportement après qu'il est effectivement persistant

j'ai vraiment aprécier des commentaires sur mon mixtes solution 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;
    }
-1
répondu user2083808 2014-02-07 17:33:56

dans la pratique, il semble que L'Option 2 (clé primaire) soit le plus fréquemment utilisée. Clé d'affaires naturelle et immuable est rarement chose, la création et le soutien de clés synthétiques sont trop lourds pour résoudre des situations, qui ne sont probablement jamais arrivés. Jetez un oeil à spring-data-jpa AbstractPersistable mise en œuvre (la seule chose que: pour Hibernate mise en œuvre utilisation Hibernate.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 manipulation de nouveaux objets dans HashSet/HashMap. À l'opposé , L'Option 1 (reste Object implémentation) est cassée juste après merge , c'est une situation très fréquente.

si vous n'avez pas de clé d'affaires et avez un réel besoin de manipuler la nouvelle entité dans la structure de hachage, outrepasser hashCode à constante, comme ci-dessous Vlad Mihalcea a été conseillé.

-1
répondu GKislin 2016-10-03 13:53:16

ci-dessous est une solution simple (et testée) pour Scala.

  • notez que cette solution ne rentre dans aucune des 3 catégories donné dans la question.

  • toutes mes entités sont des sous-classes de L'UUIDEntity donc je suis le ne vous répétez pas le principe (sec).

  • si nécessaire, la génération UUID peut être rendue plus précise (en utilisant plus de pseudo-nombres 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()
}
-2
répondu jhegedus 2014-02-07 13:25:41