Dienstag, 16. Februar 2010

Hibernate/JPA, hashCode() und Eclipse

Entitäten in Hibernate/JPA haben oft einen synthetischen (technischen) Primärschlüssel, der im folgenden Beispiel als Long id implementiert wird. Sofern es keine zusätzlichen fachlichen Schlüsselkandidaten gibt, muss man die hashCode()-Methode für Hibernate/JPA so implementieren, dass sie ausschließlich mit dieser Id arbeitet. (Es gibt auf hibernate.org eine lange Diskussion zu diesem Thema, denn die "offiziell" vorgeschlagene Variante, "halb-eindeutige" Felder zu verwenden, macht in der Praxis meistens mehr Probleme als die Id-Variante.)

Damit man hashCode() (und das zwingend dazu gehörende equals()) nicht immer wieder von Hand neu schreiben muss, bietet Eclipse über Source > Generate hashCode() and equals() die Möglichkeit, die beiden Methoden mit wählbaren Attributen zu generieren. Eine Beispiel-Entität könnte dann wie folgt aussehen:
@Entity
public class Datensatz {

@Id @GeneratedValue
private Long id;

// weitere Felder, Getter, Setter, equals() etc. ...

/**
* Von Eclipse 3.5 generiert.
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((id == null) ? 0 : id.hashCode());
return result;
}
}
An sich liefert die Hashwert-Berechnung mittels Primzahlen eine gute Streuung, und der automatisch generierte Code beachtet auch, dass die Id null sein kann – nämlich bei noch nicht in der Datenbank gespeicherten (persistierten) Objekten.

Nun testen wir diese Entity-Klasse:
@Test
public void testAddToHashSet() {

Set<Datensatz> menge = new HashSet<Datensatz>();

Datensatz ds1 = new Datensatz();
Datensatz ds2 = new Datensatz();

assertTrue("Menge sollte leer sein", menge.isEmpty());

menge.add(ds1);

assertTrue("Menge sollte ein Element besitzen",
menge.size() == 1);
// ok

menge.add(ds2);

assertTrue("Menge sollte zwei Elemente besitzen",
menge.size() == 2);
// immer noch 1... Ups!?!
}
Immer, wenn man Assoziationen im Objekt-Modell aufbaut, bevor die Datensätze gespeichert sind (also z.B. bei Unit-Tests wie im obigen Beispiel), tritt das gezeigte Szenario auf. Wir haben hier zwei Objekte, die beide keine Id (=null) besitzen. Es sind aber trotzdem zwei nicht identische Objekte in separaten Speicherbereichen, die beim Persistieren entsprechend zwei Datensätze mit unterschiedlichen Primärschlüsseln (Ids) erzeugen würden. Leider kann sich das HashSet nur eines der beiden Objekte merken – und auch beim Persistieren einer Assoziation ginge so eines der Objekte verloren!

Eine Anpassung der generierten hashCode()-Methode ist denkbar einfach. Statt bei einer nicht vorhandenen Id die Zahl 0 für die Hashwert-Berechnung zu verwenden, nehmen wir den Hashwert des Objekts, der bei nicht identischen Objekten in den allermeisten Fällen unterschiedlich ist:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((id == null) ? super.hashCode() : id.hashCode());
return result;
}
Eine vergleichbare Implementierung nutze ich seit Jahren ohne Probleme, weshalb ich diese Variante pauschal für die praktikabelste halte. In Einzelfällen mögen natürlich speziellere Implementierungen sinnvoller sein.

Kommentare:

  1. Es sollte beachtet werden das es hier zu schweren Problemen mit hash-basierten Strukturen kommen kann,
    da sich der Hashcode während der Laufzeit verändert!
    Siehe dazu auch diesen Artikel:
    http://www.angelikalanger.com/Articles/JavaSpektrum/03.HashCode/03.HashCode.html
    Abschnitt:
    Hash-Codes und Objekt-Referenzen

    AntwortenLöschen
  2. Vielen Dank für den Link auf einen der bekanntermaßen exzellenten Artikel von Angelika Langer und Klaus Kreft!

    Der Hinweis ist technisch absolut korrekt. Es geht mir in diesem Post aber eher darum, den besten Kompromiss von equals und hashCode für ORMs wie Hibernate bzw. JPA zu finden.

    In vielen Domain-Objekten gibt es leider keinen echten Fachschlüssel, so dass sich der (synthetische) Schlüssel im Moment der Persistierung (also zur Laufzeit) ändert - von null (oder einem anderen festen Wert) auf den Schlüsselwert der Datenbank. Mit diesem Problem haben viele Implementierungen zu kämpfen - auch die von Eclipse generierten Methoden -, aber die technisch korrekten Lösungen haben oft andere Probleme (weshalb das Thema auf der Hibernate-Seite so kontrovers diskutiert wird - siehe Link am Anfang des Posts. Hm, ich merke gerade, dass JBoss beim Umstellen der Seite alle alten Kommentare entfernt hat... Schade). Beispielsweise ist es in großen, heterogenen Projekten oft nicht möglich oder nicht erwünscht, dass Java-Anwendungen einen Primärschlüsselwert für die Datenbank erzeugen.

    Ich zeige in obigem Code einfach nur, wie man die typischerweise als Kompromiss verwendete Implementierung wenigstens so programmiert, dass sie sowohl von Hibernate gut verwendet werden kann, aber auch ohne Hibernate mit Objekten nur im Hauptspeicher funktioniert - z.B. für Unit-Tests. Die Lösung ist definitiv nicht perfekt (gerade weil man Probleme mit Hash-Strukturen bekommen kann, wie ja auch der Unit-Test zeigt), aber eine pauschal bessere Implementierung habe ich leider noch nicht gesehen.

    AntwortenLöschen