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é) pourList
/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 :
- ne les annulent pas; s'appuient sur
Object.equals()
etObject.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
-
- 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
-
- 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:
- ai-je manqué une option et/ou un point pro/con?
- 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.
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.
j'ai toujours outrepassé equals/hashcode et l'implémenter sur la base de l'identification de l'entreprise. Semble la solution la plus raisonnable pour moi. Voir le lien suivant 151920920" .
pour résumer toutes ces choses, voici une liste de ce qui fonctionnera ou ne fonctionnera pas avec les différentes façons de gérer égal / hashCode:
MODIFIER :
pour expliquer pourquoi cela me convient:
- Je n'utilise généralement pas la collection basée sur le hachage (HashMap/HashSet) dans mon application JPA. Si je dois, je préfère créer la solution UniqueList.
- je pense que changer business id sur runtime n'est pas une bonne pratique pour une application 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 dans la collection basée sur le hachage.
- pour mon modèle, j'ai mis de l'entreprise id sur le constructeur et ne fournit pas de poseurs. J'ai laissé l'implémentation de JPA changer le champ au lieu de la propriété.
- UUID solution semble être exagéré. Pourquoi UUID si vous avez une carte d'identité professionnelle? Après tout, je définirais l'unicité de l'id d'entreprise dans la base de données. Pourquoi avoir trois index pour chaque table dans la base de données alors?
nous avons habituellement deux identifiants dans nos entités:
- 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).
- est pour nos besoins d'application (
equals()
ethashCode()
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.
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.
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)
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
.
- si vous avez une business key , vous devez utiliser cette option pour
equals
/hashCode
. - 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 vousmerge
et entité. -
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 }
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();
}
}
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.
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.
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:
- Pas de champs supplémentaires dans la bd
- Pas de codage supplémentaire dans descendants des entités, une approche pour tous
- aucun problème de performance (comme avec UUID), DB Id generation
- Pas de problème avec Hashmaps (n'avez pas besoin de garder à l'esprit l'utilisation de l'égalité et etc.)
- Hashcode de la nouvelle entité n'a pas changé dans le temps même après persisting
Inconvénients:
- Il y a peut être des problèmes avec la sérialisation et la désérialisation pas persisté entités Le Hashcode
- de l'entité sauvegardée peut changer après avoir été rechargé à partir de DB
- n'a pas persisté les objets considérés comme toujours différents (peut-être est-ce juste?)
- 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();
}
}
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
etclass 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.
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?
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.
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?
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; }
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é.
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()
}