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.

2voto

aux Punkte 1547

Bitte beachten Sie den folgenden Ansatz, der auf vordefinierten Typbezeichnern und der ID basiert.

Die spezifischen Annahmen für JPA:

  • Entitäten desselben "Typs" und derselben Nicht-Null-ID werden als gleich betrachtet
  • nicht-persistierte Entitäten (unter der Annahme, dass es keine ID gibt) sind niemals gleichwertig mit 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 Beispiel einer 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());
}

Die wichtigsten Vorteile hier:

  • Einfachheit
  • stellt sicher, dass Unterklassen Typidentität bieten
  • vorhergesagtes Verhalten bei stellvertretenden Klassen

Benachteiligungen:

  • Erfordert von jeder Entität den Aufruf super()

Anmerkungen:

  • Bei der Verwendung von Vererbung ist Vorsicht geboten. Z.B. Instanzgleichheit von class A y class B extends A kann von den konkreten Einzelheiten der Anwendung abhängen.
  • Verwenden Sie idealerweise einen Geschäftsschlüssel als ID

Ich freue mich auf Ihre Kommentare.

1voto

Neil Stevens Punkte 785

Ich habe in der Vergangenheit immer Option 1 gewählt, weil ich diese Diskussionen kannte und es für besser hielt, nichts zu tun, bis ich wusste, was das Richtige ist. Diese Systeme laufen alle noch erfolgreich.

Nächstes Mal werde ich jedoch vielleicht Option 2 ausprobieren - die Verwendung der von der Datenbank generierten Kennung.

Hashcode und equals lösen eine IllegalStateException aus, wenn die id nicht gesetzt ist.

Dadurch wird verhindert, dass unerwartet subtile Fehler im Zusammenhang mit nicht gespeicherten Entitäten auftauchen.

Was halten die Menschen von diesem Ansatz?

1voto

Demel Punkte 137

Der Ansatz der Geschäftsschlüssel passt nicht zu uns. Wir verwenden DB generiert ID , vorübergehend vorübergehend tempId y Überschreiben Sie equal()/hashcode(), um das Dilemma zu lösen. Alle Entitäten sind Abkömmlinge von Entity. Pro:

  1. Keine zusätzlichen Felder in der DB
  2. Keine zusätzliche Kodierung in nachgeordneten Entitäten, ein Ansatz für alle
  3. Keine Leistungsprobleme (wie bei UUID), DB Id-Generierung
  4. Kein Problem mit Hashmaps (Sie müssen nicht auf die Verwendung von equal & etc. achten)
  5. Der Hashcode der neuen Entität ändert sich auch nach der Persistierung nicht

Nachteile:

  1. Es kann zu Problemen bei der Serialisierung und Deserialisierung von nicht persistierten Entitäten kommen
  2. Der Hashcode der gespeicherten Entität kann sich nach dem erneuten Laden aus der DB ändern
  3. Nicht persistierte Objekte werden immer als unterschiedlich betrachtet (vielleicht ist das richtig?)
  4. Was noch?

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

1voto

Christian Beikov Punkte 10298

IMO haben Sie 3 Optionen für die Implementierung von equals/hashCode

  • Verwendung einer von der Anwendung generierten Identität, z. B. einer UUID
  • Implementierung auf der Grundlage eines Geschäftsschlüssels
  • Implementieren Sie es auf der Grundlage des Primärschlüssels

Die Verwendung einer von einer Anwendung generierten Identität ist der einfachste Ansatz, hat aber auch einige Nachteile

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

Wenn Sie damit arbeiten können Schattenseiten verwenden Sie einfach diesen Ansatz.

Um das Problem der Verknüpfung zu überwinden, könnte man die UUID als natürlichen Schlüssel und einen Sequenzwert als Primärschlüssel verwenden, aber dann könnte man immer noch auf die Probleme mit der Implementierung von equals/hashCode in untergeordneten Entitäten stoßen, die eingebettete IDs haben, da man auf der Grundlage des Primärschlüssels verknüpfen möchte. Die Verwendung des natürlichen Schlüssels in den untergeordneten Entitäten id und des Primärschlüssels für den Verweis auf die übergeordnete Entität 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) liefert, den Sie mit externen Systemen teilen können, ohne Systeminterna preiszugeben.

Die Implementierung auf der Grundlage eines geschäftlichen Schlüssels, wenn man das von einem Nutzer erwarten kann, ist eine nette Idee, hat aber auch ein paar Nachteile

In den meisten Fällen ist dieser Geschäftsschlüssel eine Art von Code die der Benutzer angibt, und seltener eine Kombination aus mehreren Attributen.

  • Joins sind langsamer, weil Joins auf der Grundlage von Text variabler Länge einfach langsam sind. Einige DBMS können sogar Probleme haben, einen Index zu erstellen, wenn der Schlüssel eine bestimmte Länge überschreitet.
  • Meiner Erfahrung nach ändern sich geschäftliche Schlüssel häufig, was kaskadierende Aktualisierungen von Objekten, die sich auf sie beziehen, erforderlich macht. Dies ist unmöglich, wenn externe Systeme darauf verweisen.

IMO sollten Sie nicht ausschließlich einen Geschäftsschlüssel einführen oder damit arbeiten. Es ist ein nettes Add-on, d.h. die Benutzer können schnell nach diesem geschäftlichen Schlüssel suchen, aber das System sollte sich nicht darauf verlassen, dass es funktioniert.

Die Implementierung auf der Grundlage des Primärschlüssels ist nicht ganz unproblematisch, aber vielleicht ist das auch gar nicht so schlimm.

Wenn Sie IDs für externe Systeme bereitstellen müssen, verwenden Sie den von mir vorgeschlagenen UUID-Ansatz. Ist dies nicht der Fall, können Sie immer noch den UUID-Ansatz verwenden, müssen es aber nicht. Das Problem bei der Verwendung einer vom DBMS generierten ID in equals/hashCode ergibt sich aus der Tatsache, dass das Objekt vor der Zuweisung der ID zu hashbasierten Sammlungen hinzugefügt worden sein könnte.

Der offensichtliche Weg, um dies zu umgehen, ist einfach nicht das Objekt zu Hash-basierten Sammlungen hinzufügen, bevor Sie die ID zuweisen. Mir ist klar, dass dies nicht immer möglich ist, weil Sie vielleicht schon vor der Zuweisung der ID eine Deduplizierung wünschen. Um die hashbasierten Sammlungen dennoch verwenden zu können, müssen Sie die Sammlungen nach der Zuweisung der ID einfach neu erstellen.

Sie könnten etwa so vorgehen:

@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 Pre- und Post-Persist-Ereignissen funktioniert, aber die Idee ist:

  • Vorübergehendes Entfernen des Objekts aus hashbasierten Sammlungen
  • Bestehen Sie darauf
  • Erneutes Hinzufügen des Objekts zu den hashbasierten Sammlungen

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

Letztendlich liegt es an Ihnen. Ich persönlich verwende die meiste Zeit den sequenzbasierten Ansatz und verwende den UUID-Ansatz nur, wenn ich einen Bezeichner für externe Systeme bereitstellen muss.

0voto

Christopher Yang Punkte 3589

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. Er betrifft die Art und Weise, wie eine Organisation auf eine Entität verweist und wie ihre Clients auf dieselbe Entität verweisen. Ich habe so viele Probleme mit einem fehlenden Geschäftsschlüssel gesehen, dass ich Folgendes geschrieben habe mein eigener Blog um meinen Standpunkt zum Ausdruck zu bringen.

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

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