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.

29voto

Vlad Mihalcea Punkte 121171
  1. Wenn Sie einen geschäftlichen Schlüssel haben, sollten Sie diesen verwenden für equals y hashCode .

  2. Wenn Sie keinen Geschäftsschlüssel haben, sollten Sie ihn nicht auf der Standardeinstellung belassen Object equals- und hashCode-Implementierungen, weil das nicht mehr funktioniert, nachdem Sie merge und Unternehmen.

  3. Sie können den Entitätsbezeichner in der equals Methode nur, wenn die hashCode Implementierung gibt einen konstanten Wert zurück, etwa so:

    @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 getClass().hashCode();
        }
    
        //Getters and setters omitted for brevity
    }

Sehen Sie sich das an Testfall auf GitHub die beweist, dass diese Lösung hervorragend funktioniert.

16voto

jbyler Punkte 6558

Obwohl die Verwendung eines Geschäftsschlüssels (Option 3) die am häufigsten empfohlene Vorgehensweise ist ( Hibernate-Gemeinschafts-Wiki "Java Persistence with Hibernate" S. 398), und das ist es, was wir meistens benutzen, gibt es einen Hibernate-Bug, der dies für eager-fetched sets bricht: HHH-3799 . In diesem Fall kann Hibernate eine Entität zu einer Menge hinzufügen, bevor ihre Felder initialisiert werden. Ich bin mir nicht sicher, warum diesem Fehler nicht mehr Aufmerksamkeit geschenkt wurde, 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 Zustand basieren sollten (Referenz Odersky et al. ), und eine Hibernate-Entität mit Hibernate-verwaltetem Primärschlüssel hat keine einen solchen unveränderlichen Zustand. Der Primärschlüssel wird von Hibernate geändert, wenn ein transientes Objekt persistent wird. Der Business-Schlüssel wird von Hibernate ebenfalls geändert, wenn es ein Objekt initialisiert, das sich im Prozess der Initialisierung befindet.

Damit bleibt nur noch Option 1, die Vererbung der java.lang.Object-Implementierungen auf der Grundlage der Objektidentität, oder die Verwendung eines von der Anwendung verwalteten Primärschlüssels, wie er von James Brundege in "Lass den Winterschlaf nicht deine Identität stehlen" (auf die bereits in der Antwort von Stijn Geukens verwiesen wurde) und von Lance Arlaus in "Objektgenerierung: Eine bessere Herangehensweise an die Hibernate-Integration" .

Das größte Problem bei Option 1 ist, dass losgelöste Instanzen nicht mit persistenten Instanzen mittels .equals() verglichen werden können. Aber das ist OK; der Vertrag von equals und hashCode überlässt es dem Entwickler zu entscheiden, was Gleichheit für jede Klasse bedeutet. Also lassen Sie equals und hashCode einfach von Object erben. Wenn Sie eine losgelöste Instanz mit einer persistenten Instanz vergleichen müssen, können Sie eine neue Methode explizit für diesen Zweck erstellen, etwa boolean sameEntity o boolean dbEquivalent o boolean businessEquals .

7voto

Martin Andersson Punkte 15861

Jakarta Persistence 3.0, Abschnitt 4.12 schreibt:

Zwei Entitäten desselben abstrakten Schematyps sind dann und nur dann gleich, wenn sie denselben Primärschlüsselwert haben.

Ich sehe keinen Grund, warum sich Java-Code anders verhalten sollte.

Wenn sich die Entitätsklasse in einem so genannten "transienten" Zustand befindet, d.h. sie ist noch nicht persistiert und hat keinen Bezeichner, dann können die hashCode/equals-Methoden keinen Wert zurückgeben, sie sollten sich auflösen, idealerweise implizit mit einer NullPointerException wenn die Methode versucht, die ID zu durchlaufen. So oder so wird der Anwendungscode daran gehindert, eine nicht verwaltete Entität in eine Hash-basierte Datenstruktur aufzunehmen. Warum sollte man nicht noch einen Schritt weiter gehen und auflösen, wenn die Klasse und der Bezeichner gleich sind, aber andere wichtige Attribute wie die version ungleich sind ( IllegalStateException )! Ausfallsicherheit auf deterministische Weise ist immer die bevorzugte Option.

Ein Wort der Warnung: Dokumentieren Sie auch das Aufblasverhalten. Dokumentation ist an und für sich wichtig, aber es wird hoffentlich auch verhindern, dass jüngere Entwickler in der Zukunft etwas Dummes mit Ihrem Code machen (sie haben diese Tendenz, NullPointerException zu unterdrücken, wo es passiert ist und das letzte, was sie im Kopf haben, sind Seiteneffekte lol).

Oh, und verwenden Sie immer getClass() anstelle von instanceof . Die Gleichheits-Methode erfordert Symmetrie. Wenn b ist gleich a alors a muss gleich sein mit b . Mit Unterklassen, instanceof bricht diese Beziehung ( a es no Instanz von b ).

Obwohl ich persönlich immer die getClass() auch bei der Implementierung von Nicht-Entitätsklassen (der Typ es Zustand, und somit eine Unterklasse fügt Zustand auch wenn die Unterklasse leer ist oder nur Verhalten enthält), instanceof wäre nur dann in Ordnung gewesen, wenn die Klasse endgültig . Entitätsklassen dürfen jedoch nicht final sein ( §2.1 ), so dass uns hier wirklich nichts anderes übrig bleibt.

Manche Leute mögen vielleicht nicht getClass() weil der Proxy des Persistenzanbieters das Objekt umhüllt. Dies konnte in der Vergangenheit ein Problem sein, sollte es aber eigentlich nicht sein. Ein Anbieter, der nicht verschiedene Proxy-Klassen für verschiedene Entitäten zurückgibt, nun, ich würde sagen, das ist kein sehr intelligenter Anbieter lol. Generell sollten wir ein Problem erst dann lösen, wenn es ein Problem gibt. Und es scheint, dass die Dokumentation von Hibernate es nicht einmal für erwähnenswert hält. In der Tat, sie verwenden elegant getClass() in ihren eigenen Beispielen ( siehe dies ).

Wenn man schließlich eine Entitätsunterklasse hat, die eine Entität ist, und die verwendete Vererbungsabbildungsstrategie nicht der Standard ("einzelne Tabelle") ist, sondern als "verbundener Subtyp" konfiguriert ist, dann ist der Primärschlüssel in dieser Unterklassentabelle wird gleich sein als Oberklassentabelle. Wenn die Mapping-Strategie "Tabelle pro konkreter Klasse" ist, dann ist der Primärschlüssel kann derselbe sein wie in der Oberklasse. Eine Entitätsunterklasse fügt höchstwahrscheinlich einen Zustand hinzu und ist daher wahrscheinlich logisch gesehen ein anderes Ding. Aber eine gleichwertige Implementierung mit instanceof kann sich nicht notwendigerweise und sekundär nur auf die ID stützen, da sie, wie wir gesehen haben, für verschiedene Einrichtungen gleich sein kann.

Meiner Meinung nach, instanceof hat in einer nicht-finalen Java-Klasse überhaupt nichts zu suchen, niemals. Dies gilt insbesondere für persistente Entitäten.

6voto

Drew Punkte 1302

Ich stimme mit Andrews Antwort überein. Wir tun 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 Sache, die zu berücksichtigen ist, ist, dass die Aufrufe von UUID.randomUUID() ziemlich langsam sind, so dass Sie vielleicht die UUID nur bei Bedarf generieren möchten, z. B. während der Persistenz oder bei 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();
    }

}

2voto

Adam Gent Punkte 45977

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

Wir unternehmen nichts (d.h. wir setzen nichts außer Kraft).

Wenn wir Equals/Hashcode für Sammlungen benötigen, verwenden wir UUIDs. Sie erstellen die UUID einfach im Konstruktor. Wir verwenden http://wiki.fasterxml.com/JugHome für UUID. UUID ist ein wenig teurer CPU weise aber ist billig im Vergleich zu Serialisierung und db Zugriff.

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