Wenn man mit Datenbank-Objekten arbeitet kommt man nicht herum, sich genauere Gedanken um das Thema equals und hashCode zu machen.

Wenn man die Implementierung sich selbst überlässt, also die Vorgaben durch Object nicht überschriebt, dann gelten zwei Variablen nur als gleich, wenn die auf das gleiche Objekt zeigen. Diese initiale Implementierung ist im Umfeld von Datenbanken aber nicht sonderlich hilfreich, da durchaus ein Objekt zwei mal aus der Datenbank geladen worden sein kann. Und obwohl es sich fachlich um das gleiche Objekt handelt, würden sie nicht auf die gleiche Instanz im Speicher zeigen. Das gleiche Problem hätte man, wenn man durch ein Deep-Copy oder eine Serialisierung einen Datensatz zum zweiten Mal lädt.

Primarykey als Identität

Ein sinnvollerer Vergleich wäre im Umfeld von Datenbanken der Primärschlüssel des Datensatzes:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || !getClass().isAssignableFrom(o.getClass())) return false;
    MyEntity other = (MyEntity) o;
    return getId() != null ? getId().equals(other.getId()) : other.getId() == null;
}
 
@Override
public int hashCode() {
    return getId() != null ? getId().hashCode() : 0;
}

Mit dieser Implementierung kommt man schon sehr weit: Alle contains-Abfragen an Collections liefern richtige Ergebnisse und man kann die Entitäten in Sets ablegen und sicher sein, dass nur eine Instanz mit dieser ID im Set vorkommt.

Aber leider hat diese Lösung auch einen Nachteil: Werden Objekte neu erzeugt und sind noch nicht persistiert, besitzen sie üblicherweise noch gar keinen Primärschlüssel. Und wenn die ID null ist, dann funktionieren equals und hashCode auch nicht mehr.
In der Dokumentation von Hibernate wird ausdrücklich auf diesen Umstand hingewiesen und davon abgeraten, den Datenbank-Primarykey als Grundlage für equals und hashCode zu nehmen, denn equals und hashCode sollten über den gesamten Lebenszyklus eines Objektes eindeutig und unveränderbar (immutable) sein. Dies ist aber nicht der Fall, solange ein neu erzeugtes Objekt noch nicht gespeichert ist.

Fachlicher Schlüssel

Die Hibernate Dokumentation empfiehlt an dieser Stelle die Benutzung eines fachlichen Schlüssels (Businesskey), der eine Zusammensetzung von eindeutigen und unveränderlichen Attributen sein sollte. In vielen Fällen ist dies möglich und einfach. So kann man in einer Länder-Enumeration-Tabelle den zwei Buchstaben ISO-Code eines Landes auch als Naturalkey oder Businesskey benutzen:

public class Country {
  @Id
  private Integer id;
 
  @Column(name = "iso3166_2", nullable=false, unique=true)
  private String iso3166_2;
 
  public Country(String iso3166_2){
    this.iso3166_2 = iso3166_2;
  }
 
  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || !getClass().isAssignableFrom(o.getClass())) return false;
      Country other = (Country) o;
      return iso3166_2.equals(other.iso3166_2);
}
 
  @Override
  public int hashCode() {
    return iso3166_2.hashCode();
  }
}

Unabhängige ID

Aber leider gibt es auch Fälle, bei denen ein Businesskey nicht vorhanden ist. In solchen Fällen wird im Laufe einer ähnlichen Diskussion, sowie in einem weiteren Artikel das Konzept der Surrogatschlüssel (surrogate key) empfohlen.
Hierbei wird für jedes Objekt bereits bei der Erzeugung ein eindeutiger, unveränderlicher Schlüssel definiert, meist eine UUID. Dieser wird beim Persistieren des Objektes immer mit persistiert und übertragen. Er existiert parallel zum Primarykey der Datenbank. So ist das Objekt ab Erzeugung identifizierbar. Dafür hat man ein fachlich „bedeutungsloses“ Feld im Model.

public class MyEntity {
  @Id
  private Integer id;
 
  @Column(name = "uuid", nullable=false)
  private String uuid;
 
  public MyEntity() {
    this.uuid = UUID.randomUUID();
  }
 
  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || !getClass().isAssignableFrom(o.getClass())) return false;
      MyEntity other = (MyEntity) o;
      return uuid.equals(other.uuid);
}
 
  @Override
  public int hashCode() {
    return uuid.hashCode();
  }
}

Theoretisch kann man diesen Schlüssel auch als Datenbank Primärschlüssel nehmen, wobei ein alphanumerisches Feld sicherlich keine optimale Performanz liefert.

Unabhängige ID und Sequenzen

Eine andere Variante, den bei Erzeugung zugewiesenen Identifikator zu definieren, wäre den Datenbank-Primärschlüssel per Sequenz zu generieren.
Hierzu kann man sich der Sequence preallocation Technik bedienen und von der Datenbank einen Block an IDs reservieren, und diese dann per Counter in Memory an neue Objekte zuweisen.
Dieses Vorgehen ist ähnlich dem Pooled Generator für Hibernate, wobei aber Hibernate die ID stets erst zur Persistierung zuweist.
Hierfür ist es natürlich hilfreich, die Objekte von einer Factory erzeugen zu lassen, welche den Zugriff auf die Sequenz und das Zuweisen an das Objekt kapselt.

public class MyEntityFactory {
  private SequenceValueGetter sequenceValueGetter;
  private long nextId = 1;
  // Größe des reservierten Blocks
  private int blockSize = 50;
  // Erste ID aus dem reservierten Block
  private long blockStart = 1;
  private boolean initialized = false;
 
  public MyEntity create() {
    MyEntity entity = new MyEntity();
    entity.setId(getNextId());
    return entity;
  }
 
  // Synchronisierung sehr wichtig, da es sonst zu Problemen mit
  // mehreren Threads kommen kann
  @PostConstruct
  private synchronized void initialize() {
    if (!initialized) {
      blockStart = sequenceValueGetter.getNextBlock(blockSize);
      nextId = blockStart;
      initialized = true;
    }
  }
 
  // Synchronisierung sehr wichtig, da es sonst zu Problemen mit
  // mehreren Threads kommen kann
  private synchronized long getNextId() {
    if (!initialized) {
      initialize();
    }
    long id = nextId;
    nextId++;
    if (blockStart + blockSize < nextId) {
      blockStart = sequenceValueGetter.getNextBlock(blockSize);
      nextId = blockStart;
    }
    return id;
  }
}

Leider unterstützt nicht jede Datenbank die Definition von Sequenzen. So muss man bei MySQL eine Prozedur schreiben, welche Sequenzen simuliert.
Im Prinzip muss man aber den SequenceValueGetter je nach Datenbank unterschiedlich implementieren.

Identität im Kontext

Doch damit ist das Problem leider noch nicht ganz gelöst. So stellt sich die Frage, ob das equals in einem anderen Kontext eine andere Bedeutung haben kann. Angenommen, wir schreiben einen Testcase, in dem ein verändertes Objekt mit dessen original Version abgeglichen werden soll. Hierfür steht uns die jUnit-Methode Assert.assertEquals zur Verfügung. Würden wir die equals-Methode mit einem Surrogatschlüssel überschreiben, so wären zwei Objekte mit unterschiedlichen Attributwerten aus Sicht des Tests gleich.

Die Identität zweier Objekte ist also auch vom Kontext abhängig.

Absprachen

Letztendlich bleibt es, im Team konstante Regeln für die beiden Methoden equals und hashCode zu verabreden.

Aus meiner Erfahrung liefert das Benutzen von Sequenzblöcken eine sehr gute Performanz.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.