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.