Das JPA-Dilemma hashCode () / equals ()


311

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

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

  • hashCode()/equals() Vertrag Konformität (Unveränderlichkeit) für List/ SetOperationen
  • Ob identische Objekte (z. B. aus verschiedenen Sitzungen, dynamische Proxys aus träge geladenen Datenstrukturen) erkannt werden können
  • Gibt an, ob sich Entitäten im getrennten (oder nicht persistenten) Zustand korrekt verhalten

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

  1. Überschreibe sie nicht; verlassen Sie sich auf Object.equals()undObject.hashCode()
    • hashCode()Ich equals()arbeite
    • kann keine identischen Objekte identifizieren, Probleme mit dynamischen Proxys
    • Keine Probleme mit getrennten Entitäten
  2. Überschreiben Sie sie basierend auf dem Primärschlüssel
    • hashCode()Ich equals()bin kaputt
    • korrekte Identität (für alle verwalteten Entitäten)
    • Probleme mit getrennten Einheiten
  3. Überschreiben Sie sie basierend auf der Geschäfts-ID (Nicht-Primärschlüsselfelder; was ist mit Fremdschlüsseln?)
    • hashCode()Ich equals()bin kaputt
    • korrekte Identität (für alle verwalteten Entitäten)
    • Keine Probleme mit getrennten Entitäten

Meine Fragen sind:

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



UPDATE 1:

Mit " hashCode()/ equals()sind fehlerhaft" meine ich, dass aufeinanderfolgende hashCode()Aufrufe möglicherweise unterschiedliche Werte zurückgeben, die (bei korrekter Implementierung) nicht im Sinne der ObjectAPI-Dokumentation fehlerhaft sind, sondern Probleme beim Abrufen einer geänderten Entität aus einem Map, verursachen. Set oder andere Hash-basiert Collection. Folglich funktionieren JPA-Implementierungen (zumindest 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.


4
Ich verstehe nicht, was du mit "hashCode () / equals () kaputt"
nanda

4
In diesem Sinne wären sie dann nicht "kaputt", da Sie in Option 2 und 3 sowohl equals () als auch hashCode () mit derselben Strategie implementieren würden.
Matt B

11
Dies gilt nicht für Option 3. hashCode () und equals () sollten dieselben Kriterien verwenden. Wenn sich eines Ihrer Felder ändert, gibt die hashcode () -Methode für dieselbe Instanz einen anderen Wert zurück als zuvor. aber so wird gleich (). Sie haben den zweiten Teil des Satzes aus dem Hashcode () javadoc weggelassen: Immer wenn er während der Ausführung einer Java-Anwendung mehr als einmal für dasselbe Objekt aufgerufen wird, muss die hashCode-Methode konsistent dieselbe Ganzzahl zurückgeben, sofern keine Informationen vorliegen Wird in Gleichheitsvergleichen für das Objekt verwendet, wird geändert .
Matt B

1
Eigentlich bedeutet dieser Teil des Satzes das Gegenteil - Berufung hashcode() derselben Objektinstanz sollte denselben Wert zurückgeben, es sei denn, in derequals() Implementierung ändern sich. Mit anderen Worten, wenn Sie drei Felder in Ihrer Klasse haben und Ihre equals()Methode nur zwei davon verwendet, um die Gleichheit von Instanzen zu bestimmen, können Sie erwarten, dass sich der hashcode()Rückgabewert ändert, wenn Sie einen der Werte dieses Felds ändern - was in Anbetracht dessen sinnvoll ist dass diese Objektinstanz nicht mehr dem Wert entspricht, den die alte Instanz darstellt.
Matt B

2
"Probleme beim Abrufen einer geänderten Entität aus einer Map, einem Set oder anderen Hash-basierten Sammlungen" ... dies sollten "Probleme beim Versuch sein, eine geänderte Entität aus einer HashMap, HashSet oder anderen Hash-basierten Sammlungen abzurufen".
nanda

Antworten:


122

Lesen Sie diesen sehr schönen Artikel zu diesem Thema: Lassen Sie nicht zu, dass der Ruhezustand Ihre Identität stiehlt .

Die Schlussfolgerung des Artikels lautet wie folgt:

Die Objektidentität ist täuschend schwer korrekt zu implementieren, wenn Objekte in einer Datenbank gespeichert bleiben. Die Probleme ergeben sich jedoch ausschließlich daraus, dass Objekte ohne ID existieren können, bevor sie gespeichert werden. Wir können diese Probleme lösen, indem wir die Verantwortung für die Zuweisung von Objekt-IDs von objektrelationalen Mapping-Frameworks wie Hibernate übernehmen. Stattdessen können Objekt-IDs zugewiesen werden, sobald das Objekt instanziiert wird. Dies macht die Objektidentität einfach und fehlerfrei und reduziert die Menge an Code, die im Domänenmodell benötigt wird.


21
Nein, das ist kein schöner Artikel. Das ist ein verdammt großartiger Artikel zu diesem Thema, und er sollte für jeden JPA-Programmierer gelesen werden müssen! +1!
Tom Anderson

2
Ja, ich benutze die gleiche Lösung. Das Nicht-Generieren der ID durch die Datenbank hat auch andere Vorteile, z. B. die Möglichkeit, ein Objekt zu erstellen und bereits andere Objekte zu erstellen, die darauf verweisen, bevor es beibehalten wird. Dies kann Latenz und mehrere Anforderungs- / Antwortzyklen in Client-Server-Apps beseitigen. Wenn Sie Inspiration für eine solche Lösung benötigen, schauen Sie sich meine Projekte an: suid.js und suid-server-java . Grundsätzlich werden suid.jsID-Blöcke abgerufen, von suid-server-javadenen Sie dann clientseitig abrufen und verwenden können.
Stijn de Witt

2
Das ist einfach verrückt. Ich bin neu im Ruhezustand von Arbeiten unter der Haube, habe Unit-Tests geschrieben und festgestellt, dass ich ein Objekt nach dem Ändern nicht aus einem Satz löschen kann. Ich bin zu dem Schluss gekommen, dass dies auf die Änderung des Hashcodes zurückzuführen ist, konnte aber nicht verstehen, wie lösen. Der Artikel ist einfach wunderschön!
XMight

Es ist ein großartiger Artikel. Für Leute, die den Link zum ersten Mal sehen, würde ich jedoch vorschlagen, dass er für die meisten Anwendungen ein Overkill ist. Die anderen 3 auf dieser Seite aufgeführten Optionen sollten das Problem auf mehrere Arten mehr oder weniger lösen.
HopeKing

1
Verwendet Hibernate / JPA die Methode equals und hashcode einer Entität, um zu überprüfen, ob der Datensatz bereits in der Datenbank vorhanden ist?
Tushar Banne

64

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

Um all diese Dinge zusammenzufassen, hier ist eine Auflistung dessen, was mit den verschiedenen Arten des Umgangs mit equals / hashCode funktionieren wird oder nicht: Geben Sie hier die Bildbeschreibung ein

BEARBEITEN :

Um zu erklären, warum das bei mir funktioniert:

  1. Normalerweise verwende ich in meiner JPA-Anwendung keine Hash-basierte Sammlung (HashMap / HashSet). Wenn ich muss, ziehe ich es vor, eine UniqueList-Lösung zu erstellen.
  2. Ich denke, das Ändern der Geschäfts-ID zur Laufzeit ist keine bewährte Methode für eine Datenbankanwendung. 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 es wieder in die Hash-basierte Sammlung einfügen.
  3. Für mein Modell habe ich die Geschäfts-ID für den Konstruktor festgelegt und keine Setter dafür bereitgestellt. Ich habe die JPA-Implementierung das Feld ändern lassen anstelle der Eigenschaft .
  4. Die UUID-Lösung scheint übertrieben zu sein. Warum UUID, wenn Sie eine natürliche Geschäfts-ID haben? Ich würde immerhin die Eindeutigkeit der Geschäfts-ID in der Datenbank festlegen. Warum dann drei Indizes für jede Tabelle in der Datenbank?

1
In dieser Tabelle fehlt jedoch eine fünfte Zeile "funktioniert mit Liste / Mengen" (wenn Sie daran denken, eine Entität, die Teil einer Menge ist, aus einer OneToMany-Zuordnung zu entfernen), die bei den letzten beiden Optionen mit "Nein" beantwortet wird, da ihr Hashcode ( ) Änderungen, die gegen seinen Vertrag verstoßen.
MRalwasser

Siehe den Kommentar zur Frage. Sie scheinen den Equals / Hashcode-Vertrag falsch zu verstehen
Nanda

1
@MRalwasser: Ich denke du meinst das Richtige, es ist einfach nicht der equals / hashCode () Vertrag selbst, der verletzt wird. Ein veränderbarer equals / hashCode verursacht jedoch Probleme mit dem Set- Vertrag.
Chris Lercher

3
@MRalwasser: Der Hashcode kann sich nur ändern, wenn sich die Geschäfts-ID ändert, und der Punkt ist, dass die Geschäfts-ID nicht ändert. Der Hashcode ändert sich also nicht und dies funktioniert perfekt mit Hash-Sammlungen.
Tom Anderson

1
Was ist, wenn Sie keinen natürlichen Geschäftsschlüssel haben? Zum Beispiel im Fall eines zweidimensionalen Punktes, Punkt (X, Y), in einer Grafikanwendungsanwendung? Wie würden Sie diesen Punkt als Entität speichern?
Jhegedus

35

Wir haben normalerweise zwei IDs in unseren Entitäten:

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

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
}

EDIT: um meinen Punkt in Bezug auf 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("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, 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 möglicherweise ist der Ansatz mit zwei Konstruktoren für Sie besser geeignet.


2
Ich glaube, dass dies eine korrekte und gute Lösung ist. Dies kann auch einen kleinen Leistungsvorteil haben, da Ganzzahlen in Datenbankindizes normalerweise besser abschneiden als UUIDs. Abgesehen davon könnten Sie wahrscheinlich die aktuelle Ganzzahl-ID-Eigenschaft entfernen und durch die (von der Anwendung zugewiesene) UUID ersetzen.
Chris Lercher

4
Wie unterscheidet sich dies von der Verwendung der Standardmethoden hashCode/ equalsMethoden für die JVM-Gleichheit und idfür die Persistenzgleichheit? Das ergibt für mich überhaupt keinen Sinn.
Behrang Saeedzadeh

2
Dies funktioniert in Fällen, in denen mehrere Entitätsobjekte auf dieselbe Zeile in einer Datenbank verweisen. Object‚s equals()zurückkehren würde falsein diesem Fall. UUID-basierte equals()Rückgabe true.
Andrew Андрей Листочкин

4
-1 - Ich sehe keinen Grund, zwei IDs und damit zwei Arten von Identität zu haben. Dies scheint mir völlig sinnlos und potenziell schädlich zu sein.
Tom Anderson

1
Entschuldigen Sie, dass Sie Ihre Lösung kritisiert haben, ohne auf eine zu verweisen, die ich bevorzugen würde. Kurz gesagt, ich würde Objekten ein einzelnes ID-Feld geben, ich würde equals und hashCode basierend darauf implementieren und ich würde seinen Wert bei der Objekterstellung generieren, anstatt beim Speichern in der Datenbank. Auf diese Weise funktionieren alle Formen des Objekts auf dieselbe Weise: nicht persistent, persistent und getrennt. Proxies im Ruhezustand (oder ähnliches) sollten ebenfalls korrekt funktionieren, und ich denke, sie müssten nicht einmal hydratisiert werden, um Gleichheits- und HashCode-Aufrufe zu verarbeiten.
Tom Anderson

31

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

Es gibt aber noch weitere Möglichkeiten (auch mit Vor- und Nachteilen):


a) hashCode / ist gleich auf der Grundlage einer Reihe von unveränderlichen , nicht null , Konstruktor zugewiesen Felder

(+) Alle drei Kriterien sind garantiert

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

(-) erschwert die Handhabung, wenn Sie eine davon ändern müssen


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

(+) Alle drei Kriterien sind garantiert

(-) Sie können einfache zuverlässige ID-Generierungsstrategien wie DB-Sequenzen nicht nutzen

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


c) hashCode / equals basierend auf einer UUID vom Konstruktor der Entität zugewiesenen

(+) Alle drei Kriterien sind garantiert

(-) Overhead der UUID-Generierung

(-) kann ein geringes Risiko darstellen, dass je nach verwendetem Algorithmus doppelt dieselbe UUID verwendet wird (kann durch einen eindeutigen Index für DB erkannt werden)


Ich bin Fan von Option 1 und Ansatz C auch. Tun Sie nichts, bis Sie es unbedingt brauchen. Dies ist der agilere Ansatz.
Adam Gent

2
+1 für Option (b). IMHO, wenn eine Entität eine natürliche Geschäfts-ID hat, sollte dies auch ihr Datenbank-Primärschlüssel sein. Das ist einfach, unkompliziert und ein gutes Datenbankdesign. Wenn es keine solche ID hat, wird ein Ersatzschlüssel benötigt. Wenn Sie dies bei der Objekterstellung festlegen, ist alles andere einfach. Wenn Menschen keinen natürlichen Schlüssel verwenden und nicht frühzeitig einen Ersatzschlüssel generieren, geraten sie in Schwierigkeiten. Was die Komplexität bei der Implementierung betrifft - ja, es gibt einige. Aber wirklich nicht viel, und es kann auf eine sehr generische Weise gemacht werden, die es einmal für alle Entitäten löst.
Tom Anderson

Ich bevorzuge auch Option 1, aber wie man einen Komponententest schreibt, um die volle Gleichheit zu behaupten, ist ein großes Problem, da wir die Methode equals für implementieren müssen Collection.
OOD

Tu es einfach nicht. Siehe Leg dich nicht mit dem Winterschlaf an
Alonana

29

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

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

Jetzt ist es theoretisch auch möglich, dies mit Option 3 zu erreichen, obwohl sogenannte "Business-Keys" den bösen Nachteil haben, den sie ändern können: "Alles, was Sie tun müssen, ist, die bereits eingefügten Entitäten aus dem Satz zu löschen ( s) und fügen Sie sie erneut ein. " Das ist wahr - 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 dass Sie sicherstellen müssen, dass das Update durchgeführt wird , bevor andere Dinge auftreten). 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 Sets aus derselben Hibernate-Sitzung stammen. Die Hibernate-Dokumentation macht dies in Kapitel 13.1.3 sehr deutlich . 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 unerwarteten Orten auftreten. Wenn Sie beispielsweise zwei getrennte Instanzen in denselben Satz einfügen, haben beide möglicherweise dieselbe Datenbankidentität (dh sie repräsentieren dieselbe Zeile). Die JVM-Identität ist jedoch per Definition nicht für Instanzen in einem getrennten Zustand garantiert. Der Entwickler muss die Methoden equals () und hashCode () in persistenten Klassen überschreiben und ihren eigenen Begriff der Objektgleichheit implementieren.

Es spricht sich weiterhin für Option 3 aus:

Es gibt eine Einschränkung: Verwenden Sie niemals die Datenbankkennung, um die 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 vorübergehendes Objekt persistent gemacht wird. Wenn die vorübergehende Instanz (normalerweise zusammen mit getrennten Instanzen) in einem Set gehalten wird, bricht das Ändern des Hashcodes den Vertrag des Sets.

Dies ist wahr, wenn Sie

  • Die ID kann nicht vorzeitig zugewiesen werden (z. B. mithilfe von UUIDs).
  • und dennoch möchten Sie Ihre Objekte unbedingt in Sets versetzen, während sie sich in einem vorübergehenden Zustand befinden.

Andernfalls können Sie Option 2 auswä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 Stabilität garantieren, solange sich die Objekte im selben Set befinden.

Das ist richtig. Das praktische Problem, das ich dabei sehe, ist: Wenn Sie keine absolute Stabilität garantieren können, wie können Sie dann Stabilität garantieren, "solange sich die Objekte im selben Set 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 Sie können, verwenden Sie Option 2. (Weisen Sie PK so früh wie möglich zu, da Sie die Objekte in Sätzen erst verwenden können, wenn die PK zugewiesen wurde.)
  • Wenn Sie relative Stabilität garantieren können, können Sie Option 3 verwenden. Seien Sie jedoch vorsichtig damit.

Ihre Annahme, dass sich der Primärschlüssel niemals ändert, ist falsch. Beispielsweise weist der Ruhezustand den Primärschlüssel nur zu, wenn die Sitzung gespeichert wird. Wenn Sie also den Primärschlüssel als hashCode verwenden, ist das Ergebnis von hashCode () vor dem ersten Speichern des Objekts und nach dem ersten Speichern des Objekts unterschiedlich. Schlimmer noch, bevor Sie die Sitzung speichern, haben zwei neu erstellte Objekte denselben Hashcode und können sich gegenseitig überschreiben, wenn sie zu Sammlungen hinzugefügt werden. Möglicherweise müssen Sie bei der Objekterstellung sofort ein Speichern / Löschen erzwingen, um diesen Ansatz zu verwenden.
William Billingsley

2
@William: Der Primärschlüssel einer Entität ändert sich nicht. Die id-Eigenschaft des zugeordneten Objekts kann sich ändern. Dies tritt, wie Sie erklärt haben, insbesondere dann auf, wenn ein vorübergehendes Objekt persistent gemacht wird . Bitte lesen Sie den Teil meiner Antwort sorgfältig durch, in dem ich über die Methoden equals / hashCode sagte: "Sie dürfen diese Methoden erst verwenden, wenn der Primärschlüssel festgelegt ist."
Chris Lercher

Stimme voll und ganz zu. Mit Option 2 können Sie auch Equals / Hashcode in einer Superklasse herausrechnen und von allen Entitäten wiederverwenden lassen.
Theo

+1 Ich bin neu bei JPA, aber einige der Kommentare und Antworten hier implizieren, dass die Leute die Bedeutung des Begriffs "Primärschlüssel" nicht verstehen.
Raedwald

16
  1. Wenn Sie einen Geschäftsschlüssel haben , sollten Sie diesen für equals/ verwenden hashCode.
  2. Wenn Sie keinen Geschäftsschlüssel haben, sollten Sie ihn nicht mit den Standardimplementierungen Objectequals und hashCode belassen, da dies nach Ihnen mergeund der Entität nicht funktioniert .
  3. Sie können die Entitätskennung wie in diesem Beitrag vorgeschlagen verwenden . Der einzige Haken ist, dass Sie eine hashCodeImplementierung verwenden müssen, die immer den gleichen Wert zurückgibt, wie folgt :

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

Was ist besser: (1) onjava.com/pub/a/onjava/2006/09/13/… oder (2) vladmihalcea.com/… ? Lösung (2) ist einfacher als (1). Warum sollte ich (1) verwenden? Sind die Auswirkungen von beiden gleich? Garantieren beide die gleiche Lösung?
Nimo23

Und mit Ihrer Lösung: "Der HashCode-Wert ändert sich nicht" zwischen denselben Instanzen. Dies hat das gleiche Verhalten, als wäre es die "gleiche" UUID (aus Lösung (1)), die verglichen wird. Habe ich recht?
Nimo23

1
Und die UUID in der Datenbank speichern und den Footprint des Datensatzes und im Pufferpool erhöhen? Ich denke, dies kann auf lange Sicht zu mehr Leistungsproblemen führen als der eindeutige Hashcode. Bei der anderen Lösung können Sie überprüfen, ob sie über alle Entitätsstatusübergänge hinweg Konsistenz bietet. Sie finden den Test, der dies überprüft, auf GitHub .
Vlad Mihalcea

1
Wenn Sie einen unveränderlichen Geschäftsschlüssel haben, kann der HashCode ihn verwenden und er wird von mehreren Buckets profitieren. Es lohnt sich also, ihn zu verwenden, wenn Sie einen haben. Verwenden Sie andernfalls einfach die Entitätskennung, wie in meinem Artikel erläutert.
Vlad Mihalcea

1
Ich bin froh, dass es dir gefällt. Ich habe Hunderte weiterer Artikel über JPA und Hibernate.
Vlad Mihalcea

10

Obwohl die Verwendung eines Geschäftsschlüssels (Option 3) der am häufigsten empfohlene Ansatz ist ( Hibernate-Community-Wiki , "Java-Persistenz mit Ruhezustand", S. 398), und dies ist das, was wir meistens verwenden, gibt es einen Hibernate-Fehler, der dies für eifrige Abrufe unterbricht Sätze: HHH-3799 . In diesem Fall kann Hibernate einer Gruppe eine Entität 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 einem unveränderlichen Status basieren sollten (Referenz Odersky et al. ), Und eine Hibernate-Entität mit einem Hibernate-verwalteten Primärschlüssel hat keinen solchen unveränderlichen Status. Der Primärschlüssel wird vom Ruhezustand geändert, wenn ein vorübergehendes Objekt persistent wird. Der Geschäftsschlüssel wird auch von Hibernate geändert, wenn ein Objekt während der Initialisierung hydratisiert wird.

Damit bleibt nur Option 1 übrig, die die Implementierungen von java.lang.Object basierend auf der Objektidentität erbt oder einen anwendungsverwalteten Primärschlüssel verwendet, wie von James Brundege in "Lassen Sie den Ruhezustand Ihre Identität nicht stehlen" (bereits in der Antwort von Stijn Geukens erwähnt) vorgeschlagen wurde ) und von Lance Arlaus in " Objektgenerierung : Ein besserer Ansatz für die Integration im 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 für diesen Zweck erstellen, vielleicht boolean sameEntityoder boolean dbEquivalentoder boolean businessEquals.


5

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

Eine weitere zu berücksichtigende Sache ist, dass Aufrufe von UUID.randomUUID () ziemlich langsam sind. Daher sollten Sie die UUID nur bei Bedarf träge generieren, z. B. während der Persistenz oder beim Aufrufen von 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();
    }

}

Wenn Sie equals () / hashCode () überschreiben, müssen Sie ohnehin für jede Entität eine UUID generieren (ich gehe davon aus, dass Sie jede Entität, die Sie in Ihrem Code erstellen, beibehalten möchten). Sie tun dies nur einmal - bevor Sie es zum ersten Mal in einer Datenbank speichern. Danach wird die UUID nur noch vom Persistence Provider geladen. Daher sehe ich keinen Grund, es faul zu machen.
Andrew Андрей Листочкин

Ich habe Ihre Antwort positiv bewertet, weil ich Ihre anderen Ideen wirklich mag: UUID als Zahlenpaar in der Datenbank speichern und nicht in einen bestimmten Typ innerhalb der equals () -Methode umwandeln - das ist wirklich ordentlich! Ich werde diese beiden Tricks in Zukunft definitiv anwenden.
Andrew Андрей Листочкин

1
Danke für die Abstimmung. Der Grund für die träge Initialisierung der UUID war, dass wir in unserer App viele Entitäten erstellen, die niemals in eine HashMap eingefügt oder beibehalten werden. Wir haben also beim Erstellen des Objekts einen 100-fachen Leistungsabfall festgestellt (100.000 davon). Daher initiieren wir die UUID nur, wenn sie benötigt wird. Ich wünschte nur, es gäbe eine gute Unterstützung in MySql für 128-Bit-Zahlen, damit wir die UUID auch für die ID verwenden könnten und uns nicht um das auto_increment kümmern.
Drew

Oh, ich verstehe. In meinem Fall deklarieren wir nicht einmal das UUID-Feld, wenn die entsprechende Entität nicht in Sammlungen aufgenommen wird. Der Nachteil ist, dass wir es manchmal hinzufügen müssen, weil sich später herausstellt, dass wir sie tatsächlich in Sammlungen aufnehmen müssen. Dies passiert manchmal während der Entwicklung, ist uns aber glücklicherweise nach der ersten Bereitstellung bei einem Kunden nie passiert, sodass es keine große Sache war. Wenn dies nach der Inbetriebnahme des Systems geschehen würde, benötigen wir eine Datenbankmigration. Lazy UUID sind in solchen Situationen sehr hilfreich.
Andrew Андрей Листочкин

Vielleicht sollten Sie auch den schnelleren UUID-Generator ausprobieren, den Adam in seiner Antwort vorgeschlagen hat, wenn die Leistung in Ihrer Situation ein kritisches Problem darstellt.
Andrew Андрей Листочкин

3

Wie andere Leute, die viel schlauer sind als ich, bereits betont haben, gibt es eine Vielzahl von Strategien. Es scheint jedoch so zu sein, dass die meisten angewandten Designmuster versuchen, ihren Weg zum Erfolg zu hacken. Sie beschränken den Konstruktorzugriff, wenn sie Konstruktoraufrufe mit speziellen Konstruktoren und Factory-Methoden nicht vollständig behindern. In der Tat ist es mit einer klaren API immer angenehm. Aber wenn der einzige Grund darin besteht, dass die Überschreibungen für Gleichheits- und Hashcode mit der Anwendung kompatibel sind, frage ich mich, ob diese Strategien mit KISS (Keep It Simple Stupid) übereinstimmen.

Für mich überschreibe ich gerne Equals und Hashcode, indem ich die ID überprüfe. Bei diesen Methoden muss die ID nicht null sein und dieses Verhalten gut dokumentieren. Somit wird es der Entwicklervertrag, eine neue Entität beizubehalten, bevor sie an einem anderen Ort gespeichert wird. Ein Antrag, der diesen Vertrag nicht einhält, schlägt (hoffentlich) innerhalb einer Minute fehl.

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


Selbst wenn in einer Datenbank Sequenzen fehlen (wie MySQL), können diese simuliert werden (z. B. Tabelle hibernate_sequence). So erhalten Sie möglicherweise immer eine ID, die für alle Tabellen eindeutig ist. +++ Aber du brauchst es nicht. Das Anrufen Object#getClass() ist wegen H. Proxies schlecht. Das Anrufen Hibernate.getClass(o)hilft, aber das Problem der Gleichheit von Entitäten unterschiedlicher Art bleibt bestehen. Es gibt eine Lösung mit canEqual , etwas kompliziert, aber brauchbar. Einverstanden, dass es normalerweise nicht benötigt wird. +++ Das Einfügen von eq / hc in die Null-ID verstößt gegen den Vertrag, ist aber sehr pragmatisch.
Maaartinus

2

Hier gibt es natürlich schon sehr informative Antworten, aber ich werde Ihnen sagen, was wir tun.

Wir tun nichts (dh nicht überschreiben).

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


1

Ich habe in der Vergangenheit immer Option 1 verwendet, weil ich mir dieser Diskussionen bewusst war und dachte, es sei besser, nichts zu tun, bis ich das Richtige wusste. Diese Systeme laufen alle noch erfolgreich.

Beim nächsten Mal kann ich jedoch Option 2 ausprobieren - unter Verwendung der von der Datenbank generierten ID.

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

Dadurch wird verhindert, dass subtile Fehler bei nicht gespeicherten Entitäten unerwartet auftreten.

Was halten die Leute von diesem Ansatz?


1

Der Business-Keys-Ansatz passt nicht zu uns. Wir verwenden die von der DB generierte ID , die temporäre transiente TempId und überschreiben gleich () / Hashcode (), um das Dilemma zu lösen. Alle Entitäten sind Nachkommen der Entität. Vorteile:

  1. Keine zusätzlichen Felder in der DB
  2. Keine zusätzliche Codierung in Nachkommen-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 gleich & usw. berücksichtigen)
  5. Der Hashcode der neuen Entität ändert sich auch nach dem Fortbestehen nicht rechtzeitig

Nachteile:

  1. Möglicherweise gibt es Probleme beim Serialisieren und Deserialisieren nicht persistenter Entitäten
  2. Der Hashcode der gespeicherten Entität kann sich nach dem erneuten Laden aus der Datenbank ändern
  3. Nicht persistierte Objekte, die immer als unterschiedlich angesehen werden (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

Bitte beachten Sie den folgenden Ansatz basierend auf der vordefinierten Typkennung und der ID.

Die spezifischen Annahmen für JPA:

  • Entitäten desselben "Typs" und derselben Nicht-Null-ID werden als gleich angesehen
  • 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;
  }
}

Beispiel für eine konkrete Entität:

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 eine Typidentität bereitstellen
  • vorhergesagtes Verhalten mit Proxy-Klassen

Nachteile:

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

Anmerkungen:

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

Ich freue mich auf Eure Kommentare.


0

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 eine Organisation auf eine Entität verweist und wie ihre Clients auf dieselbe Entität verweisen. Ich habe genug Schmerzen gesehen, weil ich keinen Geschäftsschlüssel mehr hatte, bis ich meinen eigenen Blog schrieb , um meine Meinung auszudrücken.

Kurz gesagt: Verwenden Sie eine kurze, lesbare, sequentielle ID mit aussagekräftigen Präfixen als Geschäftsschlüssel, die ohne Abhängigkeit von einem anderen Speicher als RAM generiert wird. Die Schneeflocke von Twitter ist ein sehr gutes Beispiel.


0

IMO haben Sie 3 Möglichkeiten, um equals / hashCode zu implementieren

  • Verwenden Sie eine von einer Anwendung generierte Identität, dh 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, hat jedoch einige Nachteile

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

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

Um das Join-Problem zu lösen, könnte man die UUID als natürlichen Schlüssel und einen Sequenzwert als Primärschlüssel verwenden, aber dann könnten Sie immer noch auf die Implementierungsprobleme equals / hashCode in untergeordneten Kompositionsentitäten stoßen, die eingebettete IDs haben, da Sie auf Join-Basis arbeiten möchten auf dem Primärschlüssel. Die Verwendung des natürlichen Schlüssels in der ID der untergeordneten Entitäten und des Primärschlüssels für die Bezugnahme 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 ist dies der sauberste Ansatz, da er alle Nachteile vermeidet und Ihnen gleichzeitig einen Wert (die UUID) bietet, den Sie mit externen Systemen teilen können, ohne Systeminternale freizulegen.

Implementieren Sie es basierend auf einem Geschäftsschlüssel, wenn Sie erwarten können, dass dies von einem Benutzer eine gute Idee ist, aber auch einige Nachteile mit sich bringt

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

  • Verknüpfungen sind langsamer, da Verknüpfungen basierend auf Text variabler Länge einfach langsam sind. Einige DBMS haben möglicherweise sogar Probleme beim Erstellen eines Index, wenn der Schlüssel eine bestimmte Länge überschreitet.
  • Nach meiner Erfahrung ändern sich Geschäftsschlüssel in der Regel, was kaskadierende Aktualisierungen von Objekten erfordert, die darauf verweisen. Dies ist unmöglich, wenn externe Systeme darauf verweisen

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

Die Implementierung basierend auf dem Primärschlüssel hat Probleme, aber vielleicht ist es keine so große Sache

Wenn Sie IDs für ein externes System verfügbar machen müssen, verwenden Sie den von mir vorgeschlagenen UUID-Ansatz. Wenn Sie dies nicht tun, können Sie den UUID-Ansatz weiterhin verwenden, müssen dies jedoch nicht. Das Problem bei der Verwendung einer von DBMS generierten ID in equals / hashCode beruht auf der Tatsache, dass das Objekt möglicherweise vor dem Zuweisen der ID zu Hash-basierten Sammlungen hinzugefügt wurde.

Der naheliegende Weg, dies zu umgehen, besteht darin, das Objekt einfach nicht zu Hash-basierten Sammlungen hinzuzufügen, bevor die ID zugewiesen wird. Ich verstehe, dass dies nicht immer möglich ist, da Sie möglicherweise eine Deduplizierung wünschen, bevor Sie die ID bereits vergeben. 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 den genauen Ansatz nicht selbst getestet, daher bin ich mir nicht sicher, wie das Ändern von Sammlungen in Ereignissen vor und nach dem Fortbestehen funktioniert, aber die Idee ist:

  • Entfernen Sie das Objekt vorübergehend aus Hash-basierten Sammlungen
  • Beharrt darauf
  • Fügen Sie das Objekt erneut zu den Hash-basierten Sammlungen hinzu

Eine andere Möglichkeit, dies zu lösen, besteht darin, alle Ihre Hash-basierten Modelle nach einem Update / Persist einfach neu zu erstellen.

Am Ende liegt es an Ihnen. Ich persönlich verwende die meiste Zeit den sequenzbasierten Ansatz und verwende den UUID-Ansatz nur, wenn ich eine Kennung externen Systemen aussetzen muss.


0

Mit dem neuen Stil instanceofvon Java 14 können Sie das equalsin einer Zeile implementieren .

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

Wenn UUID für viele Menschen die Antwort ist, warum verwenden wir nicht einfach Factory-Methoden aus der Business-Ebene, um die Entitäten zu erstellen und den Primärschlüssel zur Erstellungszeit zuzuweisen?

beispielsweise:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

Auf diese Weise erhalten wir vom Persistenzanbieter einen Standardprimärschlüssel für die Entität, und unsere Funktionen hashCode () und equals () können sich darauf verlassen.

Wir könnten auch die Konstrukteure des Autos für geschützt erklären und dann in unserer Geschäftsmethode reflektieren, um auf sie zuzugreifen. Auf diese Weise würden Entwickler nicht die Absicht haben, das Auto mit einem neuen zu instanziieren, sondern durch eine Fabrikmethode.

Wie wäre es damit?


Ein Ansatz, der hervorragend funktioniert, wenn Sie bereit sind, die Leistung zu nutzen, indem Sie die Guid bei der Suche nach einer Datenbank generieren.
Michael Wiles

1
Was ist mit Unit-Test Auto? In diesem Fall benötigen Sie eine Datenbankverbindung zum Testen? Außerdem sollten Ihre Domänenobjekte nicht von der Persistenz abhängen.
Jhegedus

-1

Ich habe versucht, diese Frage selbst zu beantworten und war nie ganz zufrieden mit den gefundenen Lösungen, bis ich diesen Beitrag gelesen habe und insbesondere DREW. Mir hat gefallen, wie er faul UUID erstellt und optimal gespeichert hat.

Aber ich wollte noch mehr Flexibilität hinzufügen, dh NUR UUID erstellen, wenn auf hashCode () / equals () zugegriffen wird, bevor die Entität zum ersten Mal mit den Vorteilen jeder Lösung fortbesteht:

  • equals () bedeutet "Objekt bezieht sich auf dieselbe logische Entität"
  • Verwenden Sie die Datenbank-ID so oft wie möglich, denn warum sollte ich die Arbeit zweimal ausführen (Leistungsbedenken)?
  • Verhindern Sie Probleme beim Zugriff auf hashCode () / equals () für eine noch nicht persistierte Entität und behalten Sie das gleiche Verhalten bei, nachdem sie tatsächlich persistiert wurde

Ich würde mich sehr über das Feedback zu meiner gemischten Lösung freuen

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

Was meinst du mit "UUID, die eines Tages auf dieser Entität generiert wurde, bevor ich beibehalten wurde"? Könnten Sie bitte ein Beispiel für diesen Fall geben?
Jhegedus

Könnten Sie den zugewiesenen Generationentyp verwenden? Warum ist ein Identitätsgenerierungstyp erforderlich? Hat es einen Vorteil gegenüber zugewiesen?
Jhegedus

Was passiert, wenn Sie 1) eine neue MyEntity erstellen, 2) sie in eine Liste einfügen, 3) sie dann in der Datenbank speichern und dann 4) diese Entität aus der Datenbank zurückladen und 5) versuchen, festzustellen, ob sich die geladene Instanz in der Liste befindet . Ich vermute, dass es nicht so sein wird, obwohl es sein sollte.
Jhegedus

Vielen Dank für Ihre ersten Kommentare, die mir zeigten, dass ich nicht so klar war, wie ich sollte. Erstens war "UUID, die eines Tages auf dieser Entität generiert wurde, bevor ich beibehalten wurde" ein Tippfehler ... "bevor die IT beibehalten wurde" sollte stattdessen gelesen werden. Für die anderen Bemerkungen werde ich meinen Beitrag bald bearbeiten, um zu versuchen, meine Lösung besser zu erklären.
user2083808

-1

In der Praxis scheint Option 2 (Primärschlüssel) am häufigsten verwendet zu werden. Natürliche und unveränderliche Geschäftsschlüssel sind selten. Das Erstellen und Unterstützen von synthetischen Schlüsseln ist zu schwer, um Situationen zu lösen, die wahrscheinlich nie aufgetreten sind. Hier finden Sie aktuelle Frühjahr-data-JPA AbstractPersistable Implementierung (die einzige Sache: für Hibernate Implementierung GebrauchHibernate.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;
}

Beachten Sie nur die Manipulation neuer Objekte in HashSet / HashMap. Im Gegenteil, die Option 1 ( ObjectImplementierung bleiben ) wird kurz danach unterbrochen merge, das ist eine sehr häufige Situation.

Wenn Sie keinen Geschäftsschlüssel haben und einen REALEN Wert haben, um eine neue Entität in der Hash-Struktur zu manipulieren, überschreiben Sie diese hashCodeauf eine Konstante, wie unten von Vlad Mihalcea empfohlen.


-2

Im Folgenden finden Sie eine einfache (und getestete) Lösung für Scala.

  • Beachten Sie, dass diese Lösung nicht in eine der drei in der Frage angegebenen Kategorien passt.

  • Alle meine Entitäten sind Unterklassen der UUIDEntity, daher folge ich dem DRY-Prinzip (Don't-Repeat-Yourself).

  • Bei Bedarf kann die UUID-Generierung präziser gestaltet werden (durch Verwendung von mehr Pseudozufallszahlen).

Scala Code:

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()
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.