it-swarm.com.de

Das JPA-Dilemma hashCode ()/equals ()

Es gab einigeDiskussionen hier über JPA-Entitäten und welche Implementierung von hashCode()/equals() für JPA-Entitätsklassen verwendet werden sollte. Die meisten (wenn nicht alle) von ihnen hängen von Hibernate ab, aber ich würde sie gerne JPA-implementierungsneutral diskutieren (ich benutze übrigens EclipseLink).

Alle möglichen Implementierungen haben ihre eigenen Vorteile und Nachteile in Bezug auf:

  • hashCode()/equals()contract conformity (Unveränderlichkeit) für Listname __/SetOperationen
  • Ob identisch Objekte (z. B. aus verschiedenen Sitzungen, dynamische Proxys aus träge geladenen Datenstrukturen) erkannt werden können
  • Verhalten von Entitäten im Zustand getrennt (oder nicht beibehalten)

Soweit ich sehen kann, gibt es drei Optionen :

  1. Überschreibe sie nicht; Verlassen Sie sich auf Object.equals() und Object.hashCode()
    • hashCode()/equals() funktionieren
    • identische Objekte können nicht identifiziert werden, Probleme mit dynamischen Proxys
    • keine Probleme mit getrennten Einheiten
  2. Überschreibe sie, basierend auf dem Primärschlüssel
    • hashCode()/equals() sind defekt
    • korrekte Identität (für alle verwalteten Einheiten)
    • probleme mit getrennten Einheiten
  3. Überschreiben Sie sie basierend auf der Geschäfts-ID (Nicht-Primärschlüssel-Felder; Was ist mit Fremdschlüsseln?)
    • hashCode()/equals() sind defekt
    • korrekte Identität (für alle verwalteten Einheiten)
    • keine Probleme mit getrennten Einheiten

Meine Fragen sind:

  1. Habe ich eine Option und/oder einen Pro/Contra-Punkt verpasst?
  2. Welche Option hast du gewählt und warum?



UPDATE 1:

Mit "hashCode()/equals() are broken" meine ich, dass aufeinanderfolgende hashCode()-Aufrufe unterschiedliche Werte zurückgeben können, die (bei korrekter Implementierung) nicht im Sinne der API-Dokumentation für Objectbeschädigt sind, aber Probleme verursachen, wenn versucht wird, eine geänderte Entität von abzurufen ein Mapname__, Setoder ein anderer hash-basierter Collectionname__. Folglich funktionieren JPA-Implementierungen (mindestens EclipseLink) in einigen Fällen nicht richtig.

UPDATE 2:

Vielen Dank für Ihre Antworten - die meisten von ihnen haben eine bemerkenswerte Qualität.
Leider bin ich mir immer noch nicht sicher, welcher Ansatz für eine reale Anwendung am besten geeignet ist oder wie ich den besten Ansatz für meine Anwendung ermitteln kann. Also werde ich die Frage offen halten und auf weitere Diskussionen und/oder Meinungen hoffen.

277
MRalwasser

Lesen Sie diesen sehr schönen Artikel zum Thema: Lassen Sie den Hibernate Ihre Identität nicht stehlen .

Die Schlussfolgerung des Artikels lautet wie folgt:

Die Objektidentität ist schwer zu implementieren, wenn Objekte bleiben in einer Datenbank erhalten. Die Probleme bleiben jedoch __. gänzlich davon abzulassen, dass Objekte ohne eine ID existieren, bevor sie .__ sind. Gerettet. Wir können diese Probleme lösen, indem wir die Verantwortung von .__ übernehmen. Zuweisen von Objekt-IDs von objektrelationalen Mapping-Frameworks wie Hibernate. Objekt-IDs können stattdessen bereits mit .__ zugewiesen werden. Objekt wird instanziiert. Dies macht die Objektidentität einfach und fehlerfrei und reduziert die Menge an Code, die im Domänenmodell benötigt wird.

101
Stijn Geukens

Ich überschreibe immer gleich/Hashcode und implementiere es basierend auf der Geschäfts-ID. Scheint die vernünftigste Lösung für mich. Siehe den folgenden link .

Um das Ganze zusammenzufassen, finden Sie hier eine Auflistung dessen, was mit den verschiedenen Möglichkeiten, mit equals/hashCode umzugehen, funktioniert oder nicht funktioniert: enter image description here

EDIT:

Um zu erklären, warum das für mich funktioniert:

  1. In meiner JPA-Anwendung verwende ich normalerweise keine Hash-basierte Sammlung (HashMap/HashSet). Wenn ich muss, ziehe ich es vor, die UniqueList-Lösung zu erstellen.
  2. Ich denke, dass das Ändern der Geschäfts-ID zur Laufzeit keine bewährte Methode für Datenbankanwendungen ist. In seltenen Fällen, in denen es keine andere Lösung gibt, würde ich eine spezielle Behandlung durchführen, z. B. das Element entfernen und in die Hash-basierte Sammlung zurücklegen.
  3. Für mein Modell habe ich die Geschäfts-ID im Konstruktor festgelegt und stellt keine Setter zur Verfügung. Ich lasse die JPA-Implementierung das -Feld anstelle der Eigenschaft ändern.
  4. UUID-Lösung scheint übertrieben zu sein. Warum UUID, wenn Sie eine natürliche Geschäfts-ID haben? Ich würde schließlich die Eindeutigkeit der Geschäfts-ID in der Datenbank festlegen. Warum haben Sie dann DREI Indizes für jede Tabelle in der Datenbank?
62
nanda

Wir haben normalerweise zwei IDs in unseren Entitäten: 

  1. Ist nur für die Persistenzschicht (damit Persistenzanbieter und Datenbank Beziehungen zwischen Objekten ermitteln können).
  2. Ist für unsere Anwendungsbedürfnisse (insbesondere equals() und hashCode())

Schau mal: 

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

BEARBEITEN: um meinen Standpunkt bezüglich der Aufrufe der setUuid()-Methode zu klären. Hier ist ein typisches Szenario:

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

Wenn ich meine Tests durchführe und die Protokollausgabe sehe, behebe ich das Problem:

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

Alternativ kann ein separater Konstruktor bereitgestellt werden:

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

Mein Beispiel würde also so aussehen:

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

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

Ich verwende einen Standardkonstruktor und einen Setter, aber es könnte sein, dass zwei Konstruktoren für Sie geeigneter sind.

Wenn Sie equals()/hashCode() für Ihre Sets verwenden möchten, in dem Sinne, dass die gleiche Entität nur einmal vorhanden sein kann, gibt es nur eine Option: Option 2. Dies liegt daran, dass sich ein Primärschlüssel für eine Entität per Definition nie ändert (wenn jemand ihn tatsächlich aktualisiert, ist er nicht mehr dieselbe Entität).

Sie sollten das wörtlich nehmen: Da Ihr equals()/hashCode() auf dem Primärschlüssel basiert, dürfen Sie diese Methoden nicht verwenden, bis der Primärschlüssel festgelegt ist. Daher sollten Sie Entitäten erst dann in die Gruppe aufnehmen, wenn ihnen ein Primärschlüssel zugewiesen wurde. (Ja, UUIDs und ähnliche Konzepte können dazu beitragen, Primärschlüssel frühzeitig zuzuweisen.)

Nun ist es theoretisch auch möglich, dies mit Option 3 zu erreichen, obwohl sogenannte "Business-Keys" den Nachteil haben, dass sie sich ändern können: "Alles, was Sie tun müssen, ist, die bereits eingefügten Entities aus dem Set zu löschen ( s) und fügen Sie sie erneut ein. " Das stimmt - aber es bedeutet auch, dass Sie in einem verteilten System sicherstellen müssen, dass dies absolut überall dort erfolgt, wo die Daten eingefügt wurden (und Sie müssen sicherstellen, dass das Update durchgeführt wird , bevor andere Dinge passieren). Sie benötigen einen ausgeklügelten Update-Mechanismus, insbesondere wenn einige Remote-Systeme derzeit nicht erreichbar sind ...

Option 1 kann nur verwendet werden, wenn alle Objekte in Ihren Gruppen aus derselben Ruhezustandssitzung stammen. Die Dokumentation zum Ruhezustand macht dies in Kapitel 13.1.3. Berücksichtigung der Objektidentität :

Innerhalb einer Sitzung kann die Anwendung sicher == zum Vergleichen von Objekten verwenden.

Eine Anwendung, die == außerhalb einer Sitzung verwendet, kann jedoch zu unerwarteten Ergebnissen führen. Dies kann sogar an unerwarteten Stellen auftreten. Wenn Sie beispielsweise zwei getrennte Instanzen in dieselbe Gruppe einfügen, haben beide möglicherweise dieselbe Datenbankidentität (d. H. Sie repräsentieren dieselbe Zeile). Die JVM-Identität ist jedoch per Definition für Instanzen in einem getrennten Zustand nicht garantiert. Der Entwickler muss die Methoden equals () und hashCode () in persistenten Klassen überschreiben und einen eigenen Begriff der Objektgleichheit implementieren.

Sie spricht sich weiterhin für Option 3 aus:

Es gibt eine Einschränkung: Verwenden Sie niemals die Datenbankkennung, um Gleichheit zu implementieren. Verwenden Sie einen Geschäftsschlüssel, der aus einer Kombination eindeutiger, normalerweise unveränderlicher Attribute besteht. Die Datenbankkennung ändert sich, wenn ein transientes Objekt persistent gemacht wird. Wenn sich die vorübergehende Instanz (normalerweise zusammen mit getrennten Instanzen) in einem Set befindet, wird durch Ändern des Hashcodes der Vertrag des Sets verletzt.

Dies ist wahr, wenn Sie

  • kann die ID nicht vorzeitig zuweisen (z. B. mithilfe von UUIDs)
  • und dennoch möchten Sie Ihre Objekte unbedingt in Gruppen zusammenfassen, während sie sich in einem vorübergehenden Zustand befinden.

Ansonsten können Sie Option 2 wählen.

Dann wird die Notwendigkeit einer relativen Stabilität erwähnt:

Attribute für Geschäftsschlüssel müssen nicht so stabil sein wie Datenbankprimärschlüssel. Sie müssen nur die Stabilität garantieren, solange sich die Objekte im selben Set befinden.

Das ist richtig. Das praktische Problem, das ich damit sehe, ist: Wenn Sie keine absolute Stabilität garantieren können, wie können Sie Stabilität garantieren, "solange sich die Objekte in derselben Menge befinden". Ich kann mir einige Sonderfälle vorstellen (wie das Verwenden von Sets nur für ein Gespräch und das anschließende Wegwerfen), aber ich würde die allgemeine Praktikabilität davon in Frage stellen.


Kurzfassung:

  • Option 1 kann nur mit Objekten innerhalb einer einzelnen Sitzung verwendet werden.
  • Wenn dies möglich ist, verwenden Sie Option 2. (Weisen Sie die PK so früh wie möglich zu, da Sie die Objekte nicht in Sätzen verwenden können, bis die PK zugewiesen wurde.)
  • Wenn Sie relative Stabilität garantieren können, können Sie Option 3 verwenden. Gehen Sie jedoch vorsichtig damit um.
29
Chris Lercher

Ich persönlich habe alle diese drei Strategien bereits in verschiedenen Projekten verwendet. Ich muss sagen, dass Option 1 meiner Meinung nach die praktikabelste in einer realen App ist. Wenn die Erfahrung gemacht wurde, dass die hashCode ()/equals () -Konformität gebrochen wird, führt dies zu vielen verrückten Fehlern, da Sie jedes Mal in Situationen enden, in denen sich das Ergebnis der Gleichheit ändert, nachdem eine Entität zu einer Sammlung hinzugefügt wurde.

Es gibt jedoch weitere Optionen (auch mit ihren Vor- und Nachteilen):


a) hashCode/equals basiert auf einer Menge von immutable, nicht null, Konstruktor zugewiesen, Feldern

(+) Alle drei Kriterien sind garantiert

(-) Feldwerte müssen verfügbar sein, um eine neue Instanz erstellen zu können

(-) erschweren die Handhabung, wenn Sie eine der Einstellungen ändern müssen


b) hashCode/equals basiert auf dem Primärschlüssel, der von der Anwendung (im Konstruktor) anstelle von JPA zugewiesen wird

(+) Alle drei Kriterien sind garantiert

(-) Sie können keine einfachen zuverlässigen ID-Generierungsstrategien wie DB-Sequenzen nutzen

(-) kompliziert, wenn neue Entitäten in einer verteilten Umgebung (Client/Server) oder App-Server-Cluster erstellt werden


c) hashCode/equals basiert auf einem vom Konstruktor der Entität zugewiesenen UUID

(+) Alle drei Kriterien sind garantiert

(-) Overhead der UUID-Generierung

(-) kann ein geringes Risiko sein, dass zweimal die gleiche UUID verwendet wird, abhängig vom verwendeten Algorythm (wird möglicherweise durch einen eindeutigen Index der Datenbank ermittelt)

26
lweller

Obwohl die Verwendung eines Geschäftsschlüssels (Option 3) der am häufigsten empfohlene Ansatz ist ( Hibernate-Community-Wiki , "Java-Persistence with Hibernate", Seite 398). Dies ist das, was wir am häufigsten verwenden. Es gibt jedoch einen Hibernate-Fehler, der dies bricht für abgefragte Sets: HHH-3799 . In diesem Fall kann der Ruhezustand eine Entität zu einer Gruppe hinzufügen, bevor ihre Felder initialisiert werden. Ich bin mir nicht sicher, warum dieser Fehler nicht mehr Beachtung gefunden hat, da er den empfohlenen Business-Key-Ansatz wirklich problematisch macht.

Ich denke, der Kern der Sache ist, dass equals und hashCode auf dem unveränderlichen Zustand basieren sollten (Referenz Odersky et al. ), und eine Hibernate-Entität mit Hibernate-verwaltetem Primärschlüssel hat no einen solchen unveränderlichen Zustand. Der Primärschlüssel wird von Hibernate geändert, wenn ein transientes Objekt persistent wird. Der Geschäftsschlüssel wird auch von Hibernate geändert, wenn ein Objekt während des Initialisierungsprozesses hydratisiert wird.

Es bleibt nur die Option 1 übrig, die Java.lang.Object-Implementierungen basierend auf der Objektidentität zu erben oder einen von James Brundege vorgeschlagenen, von der Anwendung verwalteten Primärschlüssel in "Lassen Sie den Ruhezustand Ihre Identität nicht stehlen" (bereits verwiesen) von Stijn Geukens 'Antwort) und von Lance Arlaus in "Objektgenerierung: Ein besserer Ansatz für die Integration in den Ruhezustand" .

Das größte Problem bei Option 1 besteht darin, dass getrennte Instanzen mit .equals () nicht mit persistenten Instanzen verglichen werden können. Aber das ist in Ordnung; Der Vertrag von equals und hashCode überlässt es dem Entwickler, zu entscheiden, was Gleichheit für jede Klasse bedeutet. Lassen Sie also gleich und hashCode von Object erben. Wenn Sie eine getrennte Instanz mit einer persistenten Instanz vergleichen müssen, können Sie explizit eine neue Methode erstellen, beispielsweise boolean sameEntity oder boolean dbEquivalent oder boolean businessEquals.

10
jbyler
  1. Wenn Sie einen Geschäftsschlüssel haben, sollten Sie den für equals/hashCode verwenden.
  2. Wenn Sie keinen Geschäftsschlüssel haben, sollten Sie ihn nicht mit den Standardimplementierungen Object equals und hashCode belassen, da dies nach merge und entity nicht funktioniert.
  3. Sie können die Entity-ID verwenden, wie in diesem Beitrag vorgeschlagen . Der einzige Haken ist, dass Sie eine hashCode-Implementierung verwenden müssen, die immer denselben Wert zurückgibt, z.

    @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

Ich stimme Andrews Antwort zu. Wir machen dasselbe in unserer Anwendung, aber anstatt UUIDs als VARCHAR/CHAR zu speichern, teilen wir sie in zwei lange Werte auf. Siehe UUID.getLeastSignificantBits () und UUID.getMostSignificantBits ().

Zu beachten ist außerdem, dass Aufrufe an UUID.randomUUID () ziemlich langsam sind. Daher sollten Sie die UUID nur langsam generieren, wenn dies erforderlich ist, z.

@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

Wie andere Leute schon klüger als ich schon sagte, gibt es eine Vielzahl von Strategien. Es scheint jedoch so zu sein, dass die Mehrzahl der angewandten Entwurfsmuster versuchen, ihren Erfolg zu sichern. Sie beschränken den Konstruktorzugriff, wenn sie Konstruktoraufrufe nicht vollständig mit spezialisierten Konstruktoren und Factory-Methoden behindern. In der Tat ist es immer angenehm mit einer klaren API. Aber wenn der einzige Grund darin besteht, Gleichungen und Hashcode-Überschreibungen mit der Anwendung kompatibel zu machen, frage ich mich, ob diese Strategien mit KISS (Keep It Simple Stupid) übereinstimmen.

Ich mag es, Equals und Hashcode durch die Überprüfung der ID zu überschreiben. Bei diesen Methoden muss die ID nicht null sein und dieses Verhalten gut dokumentieren. So wird es der Entwicklervertrag, eine neue Entität bestehen zu lassen, bevor sie an einem anderen Ort abgelegt wird. Eine Anwendung, die diesen Vertrag nicht einhält, würde innerhalb einer Minute (hoffentlich) fehlschlagen.

Vorsicht: Wenn Ihre Entitäten in verschiedenen Tabellen gespeichert sind und Ihr Provider eine Strategie zur automatischen Generierung des Primärschlüssels verwendet, erhalten Sie über Entitätstypen doppelte Primärschlüssel. Vergleichen Sie in einem solchen Fall auch Laufzeittypen mit einem Aufruf von Object # getClass () , wodurch es natürlich unmöglich wird, dass zwei verschiedene Typen als gleich betrachtet werden. Das passt für mich zum größten Teil gut.

3

Natürlich gibt es hier bereits sehr informative Antworten, aber ich werde Ihnen sagen, was wir tun.

Wir machen nichts (dh wir überschreiben es nicht).

Wenn wir equals/hashcode benötigen, um für Sammlungen zu arbeiten, verwenden wir UUIDs ..__ Sie erstellen die UUID einfach im Konstruktor. Wir verwenden http://wiki.fasterxml.com/JugHome für UUID. UUID ist etwas teurer als die CPU, aber im Vergleich zu Serialisierung und Datenbankzugriff billig.

2
Adam Gent

Business Keys Ansatz passt nicht zu uns. Wir verwenden von DB generierteID, temporäre transiente tempId und override equal ()/hashcode (), um das Dilemma zu lösen. Alle Entitäten sind Nachkommen der Entität. Pros:

  1. Keine zusätzlichen Felder in der DB
  2. Keine zusätzliche Codierung in untergeordneten Entitäten, ein Ansatz für alle
  3. Keine Leistungsprobleme (wie bei UUID), Generierung der DB-ID
  4. Kein Problem mit Hashmaps (Sie müssen nicht die Verwendung von Gleichem & etc. berücksichtigen.)
  5. Der Hashcode der neuen Entität ändert sich auch nach dem Bestehen nicht rechtzeitig

Nachteile:

  1. Bei der Serialisierung und Deserialisierung von nicht persistenten Entitäten können Probleme auftreten
  2. Der Hashcode der gespeicherten Entität kann sich nach dem erneuten Laden aus der DB ändern
  3. Nicht persistente Objekte werden immer als unterschiedlich betrachtet (vielleicht ist das richtig?)
  4. Was sonst?

Schauen Sie sich unseren Code an:

@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

Beachten Sie den folgenden Ansatz, der auf der vordefinierten Typenbezeichnung und der ID basiert.

Die spezifischen Annahmen für die JPA:

  • entitäten desselben "Typs" und derselben Nicht-Null-ID werden als gleich betrachtet
  • nicht persistierte Entitäten (vorausgesetzt, keine ID) sind niemals gleich anderen Entitäten

Die abstrakte Entität:

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

Konkretes Entity-Beispiel:

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

Testbeispiel:

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

Hauptvorteile hier: 

  • einfachheit
  • stellt sicher, dass Unterklassen Typidentität bereitstellen
  • vorhergesagtes Verhalten bei Proxy-Klassen

Nachteile:

  • Erfordert, dass jede Entität super() aufruft

Anmerkungen:

  • Benötigt Aufmerksamkeit bei der Verwendung von Vererbung. Z.B. Die Instanzgleichheit von class A und class B extends A kann von konkreten Details der Anwendung abhängen.
  • Verwenden Sie im Idealfall einen Geschäftsschlüssel als ID

Ich freue mich auf Eure Kommentare.

1
aux

Dies ist ein häufiges Problem in jedem IT-System, das Java und JPA verwendet. Der Schmerzpunkt geht über die Implementierung von equals () und hashCode () hinaus und beeinflusst, wie sich eine Organisation auf eine Entität bezieht und wie sich ihre Kunden auf dieselbe Entität beziehen. Ich habe genug Schmerzen gehabt, wenn ich bis zu dem Punkt, an dem ich mein eigener Blog schrieb, keinen Geschäftsschlüssel hatte, um meine Ansicht auszudrücken.

Kurz gesagt: Verwenden Sie eine kurze, von Menschen lesbare, sequentielle ID mit sinnvollen Präfixen als Geschäftsschlüssel, die ohne Abhängigkeit von einem anderen Speicher als dem RAM generiert wird. Twitter's Snowflake ist ein sehr gutes Beispiel.

0

Ich habe Option 1 in der Vergangenheit immer verwendet, weil ich mir dieser Diskussionen bewusst war und es für besser hielt, nichts zu tun, bis ich wusste, was richtig ist. Diese Systeme laufen alle noch erfolgreich.

Beim nächsten Mal kann ich jedoch Option 2 ausprobieren - die von der Datenbank generierte ID.

Hashcode und equals lösen IllegalStateException aus, wenn die ID nicht festgelegt ist.

Dadurch wird verhindert, dass subtile Fehler, die nicht gespeicherte Entitäten betreffen, unerwartet angezeigt werden.

Was denken die Leute über diesen Ansatz?

0
Neil Stevens

IMO haben Sie 3 Möglichkeiten, Equals/HashCode zu implementieren

  • Verwenden Sie eine von einer Anwendung generierte Identität, d. H. Eine UUID
  • Implementieren Sie es basierend auf einem Geschäftsschlüssel
  • Implementieren Sie es basierend auf dem Primärschlüssel

Die Verwendung einer von einer Anwendung generierten Identität ist der einfachste Ansatz, bringt jedoch einige Nachteile mit sich.

  • Joins sind langsamer, wenn sie als PK verwendet werden, da 128 Bit einfach größer als 32 oder 64 Bit sind
  • "Debuggen ist schwieriger", weil es ziemlich schwierig ist, mit eigenen Augen zu prüfen, ob einige Daten korrekt sind

Wenn Sie mit diesen Downsides arbeiten können, verwenden Sie einfach diesen Ansatz.

Um das Join-Problem zu überwinden, könnte es sein, dass die UUID als natürlicher Schlüssel und ein Sequenzwert als Primärschlüssel verwendet wird. Allerdings könnten Sie immer noch auf die Implementierungsprobleme equals/hashCode in zusammengesetzten untergeordneten Entitäten mit eingebetteten IDs stoßen, da Sie beitreten möchten auf dem Primärschlüssel. Die Verwendung des natürlichen Schlüssels in der untergeordneten Entitäts-ID und des Primärschlüssels zum Verweisen auf das übergeordnete Element ist ein guter Kompromiss.

@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 Dies ist der sauberste Ansatz, da dadurch alle Nachteile vermieden werden und Sie gleichzeitig einen Wert (die UUID) erhalten, den Sie mit externen Systemen teilen können, ohne Systemintern offen zu legen.

Implementiere es basierend auf einem Geschäftsschlüssel, wenn du davon ausgehen kannst, dass ein Benutzer eine schöne Idee ist, aber auch ein paar Nachteile hat

In den meisten Fällen handelt es sich bei diesem Geschäftsschlüssel um eine Art code, die der Benutzer bereitstellt, und seltener um eine Kombination mehrerer Attribute.

  • Verknüpfungen sind langsamer, da das Verknüpfen basierend auf Text variabler Länge einfach langsam ist. Einige DBMS haben sogar Probleme, einen Index zu erstellen, wenn der Schlüssel eine bestimmte Länge überschreitet.
  • Nach meiner Erfahrung neigen Geschäftsschlüssel dazu, sich zu ändern, was kaskadierende Aktualisierungen der darauf verweisenden Objekte erfordert. Dies ist nicht möglich, wenn externe Systeme darauf verweisen

IMO sollten Sie nicht ausschließlich einen Business Key implementieren oder damit arbeiten. Es ist ein nettes Add-On, d. H. Benutzer können schnell nach diesem Geschäftsschlüssel suchen, aber das System sollte sich beim Betrieb nicht darauf verlassen.

Implementiere es basierend auf dem Primärschlüssel, hat seine Probleme, aber vielleicht ist es nicht so eine große Sache

Wenn Sie IDs für ein externes System verfügbar machen müssen, verwenden Sie die von mir vorgeschlagene UUID-Methode. Wenn Sie dies nicht tun, können Sie immer noch die UUID-Methode verwenden, müssen dies jedoch nicht .. Das Problem der Verwendung einer von DBMS generierten ID in equals/hashCode rührt von der Tatsache her, dass das Objekt möglicherweise zu einem Hash-basierten Objekt hinzugefügt wurde Sammlungen vor dem Zuweisen der ID.

Der naheliegende Weg, dies zu umgehen, besteht darin, das Objekt vor der Zuweisung der ID nicht zu Hash-basierten Sammlungen hinzuzufügen. Ich verstehe, dass dies nicht immer möglich ist, weil Sie die Deduplizierung vor der Zuweisung der ID wünschen. Um die Hash-basierten Sammlungen weiterhin verwenden zu können, müssen Sie die Sammlungen nach dem Zuweisen der ID einfach neu erstellen.

Sie könnten so etwas tun:

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

Ich habe die genaue Vorgehensweise nicht selbst getestet, daher bin ich mir nicht sicher, wie sich das Ändern von Sammlungen in vor- und nach persistenten Ereignissen auswirkt. Die Idee ist jedoch:

  • Entfernen Sie das Objekt vorübergehend aus Hash-basierten Sammlungen
  • Behalte es
  • Fügen Sie das Objekt den Hash-basierten Sammlungen erneut hinzu

Eine andere Möglichkeit, dieses Problem zu lösen, besteht darin, nach einem Update/Persist einfach alle auf Hash basierenden Modelle neu zu erstellen.

Am Ende liegt es an dir. Ich persönlich verwende den sequenzbasierten Ansatz meistens und verwende den UUID-Ansatz nur, wenn ich einen Identifikator externen Systemen offenlegen muss.

0