396 Stimmen

Das JPA hashCode() / equals() Dilemma

Es gab bereits einige Diskussionen hier über JPA-Entitäten und welche hashCode() / equals() Implementierung sollte für JPA-Entitätsklassen verwendet werden. Die meisten (wenn nicht alle) von ihnen hängen von Hibernate ab, aber ich möchte sie JPA-Implementierung-neutral diskutieren (ich verwende übrigens EclipseLink).

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

  • hashCode() / equals() Vertrag Konformität (Unveränderlichkeit) für List / Set Operationen
  • Ob identisch Objekte (z.B. aus verschiedenen Sitzungen, dynamische Proxys aus faul geladenen Datenstrukturen) erkannt werden können
  • Ob sich Entitäten korrekt verhalten in losgelöster (oder nicht-unterbrochener) Zustand

Soweit ich sehen kann, gibt es drei Möglichkeiten :

  1. Setzen Sie sie nicht außer Kraft; verlassen Sie sich auf Object.equals() y Object.hashCode()
    • hashCode() / equals() Arbeit
    • kann identische Objekte nicht identifizieren, Probleme mit dynamischen Proxys
    • keine Probleme mit abgetrennten Einheiten
  2. Überschreiben Sie sie, basierend auf den Primärschlüssel
    • hashCode() / equals() gebrochen sind
    • korrekte Identität (für alle verwalteten Einheiten)
    • Probleme mit abgetrennten Einheiten
  3. Überschreiben Sie sie, basierend auf den Business-Id (nicht primäre Schlüsselfelder; was ist mit Fremdschlüsseln?)
    • hashCode() / equals() gebrochen sind
    • korrekte Identität (für alle verwalteten Einheiten)
    • keine Probleme mit abgetrennten Einheiten

Meine Fragen sind:

  1. Habe ich eine Option und/oder ein Pro-/Kontra-Argument übersehen?

  2. Welche Option haben Sie gewählt und warum?

UPDATE 1:

Von " hashCode() / equals() kaputt sind", meine ich, dass aufeinanderfolgende hashCode() Aufrufen unterschiedliche Werte zurückgeben können, was (bei korrekter Implementierung) nicht im Sinne der Object API-Dokumentation, die aber Probleme verursacht, wenn man versucht, eine geänderte Entität aus einer Map , Set oder andere Hash-basierte Collection . Folglich werden JPA-Implementierungen (zumindest EclipseLink) in einigen Fällen nicht korrekt funktionieren.

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. Ich werde die Frage also offen lassen und hoffe auf weitere Diskussionen und/oder Meinungen.

158voto

Stijn Geukens Punkte 15139

Lesen Sie diesen sehr schönen Artikel zu diesem Thema: Lassen Sie sich durch den Winterschlaf nicht die Identität stehlen .

Die Schlussfolgerung des Artikels lautet wie folgt:

Die Objektidentität ist trügerisch schwer korrekt zu implementieren, wenn Objekte in einer Datenbank persistiert werden. Die Probleme entstehen jedoch ausschließlich aus der Erlaubnis gespeichert werden. Wir können dieses Problem lösen Zuweisung von Objekt-IDs von objektrelationalen Mapping-Frameworks wie z.B. Hibernate. Stattdessen können Objekt-IDs zugewiesen werden, sobald das Objekt instanziiert wird. Dies macht die Objektidentität einfach und und reduziert die Menge des im Domänenmodell benötigten Codes.

70voto

nanda Punkte 24132

Ich überschreibe immer equals/hashcode und implementiere es auf der Grundlage der Geschäfts-ID. Das scheint mir die vernünftigste Lösung zu sein. Siehe das Folgende Link .

Um all diese Dinge zusammenzufassen, ist hier eine Auflistung dessen, was mit den verschiedenen Möglichkeiten zur Handhabung von equals/hashCode funktioniert oder nicht funktioniert: enter image description here

EDITAR :

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

  1. Ich verwende normalerweise keine hashed-basierte Sammlung (HashMap/HashSet) in meiner JPA-Anwendung. Wenn ich muss, ziehe ich es vor, eine UniqueList-Lösung zu erstellen.
  2. Ich denke, dass das Ändern der Geschäftskennung zur Laufzeit keine optimale Vorgehensweise für eine Datenbankanwendung ist. In seltenen Fällen, in denen es keine andere Lösung gibt, würde ich eine Sonderbehandlung durchführen, z. B. das Element entfernen und es wieder in die hashed-basierte Sammlung aufnehmen.
  3. Für mein Modell habe ich die Business-ID im Konstruktor festgelegt und bietet keine Setter dafür. Ich überlasse es der JPA-Implementierung, die Feld anstelle der Immobilie.
  4. Die UUID-Lösung scheint ein Overkill zu sein. Warum UUID, wenn Sie eine natürliche Geschäftskennung haben? Ich würde doch die Einzigartigkeit der Geschäftskennung in der Datenbank festlegen. Warum mit DREI Indizes für jede Tabelle in der Datenbank?

44voto

lweller Punkte 10763

Ich persönlich habe alle drei Strategien bereits in verschiedenen Projekten eingesetzt. Und ich muss sagen, dass Option 1 meiner Meinung nach in einer realen Anwendung am praktikabelsten ist. Meiner Erfahrung nach führt das Brechen der hashCode()/equals()-Konformität zu vielen verrückten Fehlern, da man jedes Mal in Situationen gerät, in denen sich das Ergebnis der Gleichheit ändert, nachdem eine Entität zu einer Sammlung hinzugefügt wurde.

Aber es gibt noch weitere Möglichkeiten (ebenfalls mit Vor- und Nachteilen):


a) hashCode/equals auf der Grundlage einer Reihe von unveränderlich , nicht null , Konstrukteur zugeordnet , Felder

(+) alle drei Kriterien sind gewährleistet

(-) Feldwerte müssen vorhanden sein, um eine neue Instanz zu erstellen

(-) erschwert die Handhabung, wenn Sie einen der beiden Punkte ändern müssen


b) hashCode/equals auf der Grundlage eines Primärschlüssels, der von der Anwendung (im Konstruktor) anstelle von JPA zugewiesen wird

(+) alle drei Kriterien sind gewährleistet

(-) Sie können keine einfachen, zuverlässigen Strategien zur Erzeugung von IDs wie DB-Sequenzen nutzen.

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


c) hashCode/equals basierend auf einem UUID zugewiesen durch den Konstruktor der Entität

(+) alle drei Kriterien sind gewährleistet

(-) Overhead der UUID-Generierung

(-) Es besteht ein geringes Risiko, dass zweimal dieselbe UUID verwendet wird, je nach verwendetem Algorithmus (kann durch einen eindeutigen Index in der DB erkannt werden)

37voto

Chris Lercher Punkte 36644

Wenn Sie Folgendes verwenden möchten equals()/hashCode() für Ihre Sets, in dem Sinne, dass die gleiche Entität nur einmal drin sein kann, dann gibt es nur eine Möglichkeit: Möglichkeit 2. Das liegt daran, dass eine Primärschlüssel denn eine Entität ändert sich per Definition nie (wenn jemand sie tatsächlich aktualisiert, ist es nicht mehr dieselbe Entität)

Das sollten Sie wörtlich nehmen: Da Ihr equals()/hashCode() auf dem Primärschlüssel basieren, dürfen Sie diese Methoden nicht verwenden, solange der Primärschlüssel nicht gesetzt ist. Sie sollten also keine Entitäten in die Menge aufnehmen, bevor ihnen nicht ein Primärschlüssel zugewiesen wurde. (Ja, UUIDs und ähnliche Konzepte können helfen, Primärschlüssel frühzeitig zuzuweisen).

Nun ist es theoretisch auch möglich, dies mit Option 3 zu erreichen, auch wenn die so genannten "Business-Keys" den unangenehmen Nachteil haben, dass sie sich ändern können: "Alles, was Sie tun müssen, ist, die bereits eingefügten Entitäten aus der/den Menge(n) zu löschen und sie erneut einzufügen." Das stimmt - aber es bedeutet auch, dass man in einem verteilten System sicherstellen muss, dass dies absolut überall geschieht, wo die Daten eingefügt wurden (und man muss sicherstellen, dass die Aktualisierung durchgeführt wird, bevor andere Dinge geschehen). Sie werden einen ausgeklügelten Aktualisierungsmechanismus benötigen, insbesondere wenn einige entfernte Systeme nicht erreichbar sind...

Option 1 kann nur verwendet werden, wenn alle Objekte in Ihren Sets aus derselben Hibernate-Sitzung stammen. Die Hibernate-Dokumentation verdeutlicht dies in Kapitel 13.1.3. Berücksichtigung der Objektidentität :

Innerhalb einer Sitzung kann die Anwendung sicher == verwenden, um Objekte zu vergleichen.

Eine Anwendung, die == außerhalb einer Sitzung verwendet, kann jedoch zu unerwarteten Ergebnissen führen. Dies kann sogar an einigen unerwarteten Stellen auftreten. Wenn Sie zum Beispiel zwei losgelöste Instanzen in dasselbe Set setzen, könnten beide dieselbe Datenbankidentität haben (d.h. sie repräsentieren dieselbe Zeile). Die JVM-Identität ist jedoch per Definition nicht für Instanzen in einem losgelösten Zustand garantiert. Der Entwickler muss die equals()- und hashCode()-Methoden in persistenten Klassen außer Kraft setzen und seine eigene Vorstellung von Objektgleichheit implementieren.

Sie plädiert weiterhin für die Option 3:

Es gibt einen Vorbehalt: Verwenden Sie niemals den Datenbankbezeichner, um Gleichheit zu implementieren. Verwenden Sie einen Geschäftsschlüssel, der eine Kombination aus eindeutigen, normalerweise unveränderlichen Attributen ist. Der Datenbankbezeichner ändert sich, wenn ein transientes Objekt persistent gemacht wird. Wenn die transiente Instanz (normalerweise zusammen mit abgetrennten Instanzen) in einem Set gehalten wird, bricht eine Änderung des Hashcodes den Vertrag des Sets.

Das ist wahr, si Sie

  • die ID nicht frühzeitig zuweisen kann (z. B. durch Verwendung von UUIDs)
  • und dennoch wollen Sie Ihre Objekte unbedingt in Sets unterbringen, solange sie sich im transienten Zustand befinden.

Andernfalls steht es Ihnen frei, Option 2 zu wählen.

Dann wird auf die Notwendigkeit einer relativen Stabilität hingewiesen:

Attribute für Business-Keys müssen nicht so stabil sein wie Datenbank-Primärschlüssel; Sie müssen nur so lange Stabilität garantieren, wie sich die Objekte in derselben Menge befinden.

Das ist richtig. Das praktische Problem, das ich dabei sehe, ist folgendes: Wenn man keine absolute Stabilität garantieren kann, wie kann man dann Stabilität garantieren, "solange die Objekte in derselben Menge sind". Ich kann mir einige Sonderfälle vorstellen (z. B. die Verwendung von Mengen nur für ein Gespräch und das anschließende Wegwerfen), aber ich würde die generelle Praktikabilität in Frage stellen.


Kurzfassung:

  • Option 1 kann nur für Objekte innerhalb einer einzigen Sitzung verwendet werden.
  • Wenn möglich, verwenden Sie Option 2 (weisen Sie die PK so früh wie möglich zu, da Sie die Objekte in den Sets erst verwenden können, wenn die PK zugewiesen ist).
  • Wenn Sie relative Stabilität garantieren können, können Sie Option 3 verwenden. Aber seien Sie damit vorsichtig.

36voto

Wir haben normalerweise zwei IDs in unseren Entitäten:

  1. Ist nur für die Persistenzschicht (damit Persistenzanbieter und Datenbank die Beziehungen zwischen den Objekten herausfinden können).
  2. Ist für unsere Anwendung erforderlich ( equals() y hashCode() im Besonderen)

Werfen Sie einen Blick darauf:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

EDIT: zur Klarstellung meines Standpunkts bezüglich der Anrufe bei setUuid() Methode. 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("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

Wenn ich meine Tests ausführe und die Protokollausgabe sehe, kann ich das Problem beheben:

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

Alternativ kann man auch einen separaten Konstruktor bereitstellen:

@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 wie folgt 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 vielleicht ist der Ansatz mit zwei Konstruktoren für Sie besser geeignet.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X