Verwendet man Hibernate in seinem Projekt als O/R-Mapper, zählt es zu einer der ersten Aufaben eine adäquate Implementierung von equals und hashCode für die Entitäten bereitzustellen. Eine verbreitete Möglichkeit für die HashCode-Methode ist dabei die folgende:
@Override
public int hashCode() {
return getId();
} |
Dabei liefert getId() die durch Hibernate generierte Id der Entität, sprich den PrimaryKey des Datenbank-Eintrages. Soweit verständlich. Allerdings bricht diese Implementation mit der ersten Anforderung des Hash-Code Contracts (Javadoc Auszug):
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hashtables such as those provided by
* <code>java.util.Hashtable</code>.
* <p>
* The general contract of <code>hashCode</code> is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the <tt>hashCode</tt> method
* must consistently return the same integer, provided no information
* used in <tt>equals</tt> comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the <tt>equals(Object)</tt>
* method, then calling the <code>hashCode</code> method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the <tt>hashCode</tt> method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hashtables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class <tt>Object</tt> does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java<font size="-2"><sup>TM</sup></font> programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.util.Hashtable
*/ |
Da Hibernate die Id erst erzeugt, wenn die Entität gespeichert wird, verändert sich der Hash-Code während des Lebenszykluses des Objektes. In vielen Fällen stellt das kein Problem da. Arbeitet man allerdings mit HashSets als Collection für eine Assoziation und verwendet gleichzeitig Speicher-Kaskaden, führt diese Implementierung zu Schwierigkeiten. Beispiel:
@Entity public class FunkyVO extents BaseVO { @OneToMany(mappedBy = "funky", fetch = FetchType.LAZY) @Cascade({ org.hibernate.annotations.CascadeType.ALL}) private final Set<GrooveVO> grooves= new HashSet<GrooveVO>(); } |
Und dazu folgender Pseudocode:
FunkyVO funkyVO = new FunkyVO();
GrooveVO grooveVO = new GrooveVO();
funkyVO.getGrooves().add(grooveVO);
PersistService.persist(funkyVO);
funkyVO.getGrooves().contains(grooveVO); // => false
funkyVO.getGrooves().remove(grooveVO); // => false
funkyVO.getGrooves().add(grooveVO); // => false
Da die Id erst durch das Persistieren erzeugt wird, ändert sich der Hash-Code nachdem das Objekt zur Liste hinzugefügt wurde. Die Konsequenz ist, dass das Objekt nun nicht mehr wiedergefunden wird (contains), nicht mehr entfernt werden kann (remove) und bei einem erneuten Hinzufügen auch nicht als Duplikat erkannt wird (add).
Und wie sieht die Lösung aus?
Eine Möglichkeit wäre, im ganzen Projekt darauf zu achten, dass die Entitäten erst nach dem Speichern den Listen hinzugefügt werden…
Hibernate verweist dagegen bei diesem Problem darauf, dass die generierte Id nicht in equals oder hashCode verwendet werden soll. Als Lösung wird dort ein expliziter BusinessKey vorgeschlagen, der nichts mit dem PrimaryKey zu tun hat und somit auch beim Erzeugen der Entität angelegt werden könnte.
Möchte man weiterhin den PrimaryKey benutzen, kann man alternativ auch die Id selbst generieren. Allerdings muss man Hibernate dann beibringen, anhand eines anderen Attributes zu entscheiden, ob die Entität neu ist und ein INSERT ausgeführt werden muss oder ob lediglich ein UPDATE nötig ist. Ein solches Attribut könnte das Version Attribut – welches beim OptimisticLocking verwendet wird – oder etwa ein CreatedAt Feld sein, dass beim Insert durch die Datenbank gefüllt wird.