In der Laufbahn der meisten Spring/Hibernate-Entwickler, die ich kenne, kommt früher oder später ein Punkt, an dem es kein Entrinnen mehr gibt... sie müssen einen Hibernate-Benutzertyp schreiben. Der erste ist in der Regel eine Copy'n'Paste-Variante und funktioniert im Großen und Ganzen einwandfrei. Aber wenn die Dinge nicht mehr ganz so laufen wie erwartet - Hibernate ignoriert beispielsweise Änderungen an Elementen, die vom Benutzertyp verwaltet werden - wird oft deutlich, dass man nicht ausreichend versteht, wie diese Benutzertyp-Dinger eigentlich funktionieren sollen. Zumindest ist mir das passiert. In diesem Beitrag werden wir die Hibernate UserType-Schnittstelle analysieren, die Beziehungen zwischen den verschiedenen Methoden erklären und eine Reihe von Basisbenutzertypen entwickeln, die gängige Anwendungsfälle abdecken.
Die nackte Wahrheit
Die UserType-Schnittstelle von Hibernate sieht in ihrer ganzen Pracht so aus (den komplexeren CompositeUserType lassen wir vorerst beiseite): [java] public interface UserType { public int[] sqlTypes(); public Class returnedClass(); public boolean equals(Object x, Object y) throws HibernateException; public int hashCode(Object x) throws HibernateException; public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException; public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException; public Object deepCopy(Object value) throws HibernateException; public boolean isMutable(); public Serializable disassemble(Object value) throws HibernateException; public Object assemble(Serializable cached, Object owner) throws HibernateException; public Object replace(Object original, Object target, Object owner) throws HibernateException; } [/java] Von den Methoden sind returnedClass, nullSafeSet und nullSafeGet wahrscheinlich am selbsterklärendsten. Das sind wahrscheinlich auch die, die Sie für Ihren ersten Benutzertyp optimiert haben. In der Tat sind sie vielleicht die einzigen, mit denen man sich als Implementierer eines Benutzertyps wirklich befassen möchte, und dies (fast) zu ermöglichen ist einer der Hauptzwecke der Basisklassen, die wir entwickeln werden. Was ist mit den anderen Methoden, assemble, replace und dergleichen? Diese waren früher diejenigen, deren Implementierung man halb blindlings aus dem Beispiel kopiert hat, obwohl sie jetzt, da die Javadoc für sie viel detaillierter ist, hoffentlich einfacher zu implementieren sind. Dennoch ist die folgende "Standard"-Implementierung immer noch üblich: [java] @Override public boolean isMutable() { return false; } @Override public boolean equals(Object x, Object y) throws HibernateException { return ObjectUtils.equals(x, y); } @Override public int hashCode(Object x) throws HibernateException { assert (x != null); return x.hashCode(); } @Override public Object deepCopy(Object value) throws HibernateException { Rückgabewert; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { das Original zurückgeben; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { zurückgeben; } [/java] Was ist daran falsch? Abgesehen von der Unordnung eigentlich nichts... solange Sie beabsichtigen, Ihre Objekte als unveränderlich zu behandeln.
Einen Fall aufbauen
Gut, es ist an der Zeit, diese berauschenden abstrakten Höhen zu verlassen und das übliche künstliche Beispiel auszuarbeiten. Stellen Sie sich also vor, dass Sie eine Entität mit einer StringBuilder-Eigenschaft persistieren möchten, um zum Beispiel eine häufig aktualisierte Historie zu speichern.
StringBuilder ist natürlich serialisierbar und kann daher von JPA (und damit von Hibernate) sofort verarbeitet werden. Aber was Sie in der Datenbank erhalten, ist nicht besonders praktisch: Ein Wert wie "Agent 007 hat das Geheimversteck betreten" könnte als
aced0005737200176a6176612e6c616e672e537472696e674275696c64
65723cd5fb145a4c6acb0300007870770400000024757200025b43b0266
6b0e25d84ac020000787000000024004100670065006e0074002000300
030003700200065006e007400650072006500640020007400680065002
000730065006300720065007400200068006900640065006f0075007478
Nützlich, oder?
Stattdessen tun wir das Offensichtliche und behalten die String-Darstellung des Builders bei. Nach dem bewährten Copy'n'Paste-Verfahren, das wir oben beschrieben haben, erhalten wir folgendes Ergebnis:
[java]
public class ReadableStringBuilderUserType implements UserType {
public Class<StringBuilder> returnedClass() {
return StringBuilder.class;
}
public int[] sqlTypes() {
return new int[] { Types.VARCHAR };
}
public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)
throws HibernateException, SQLException {
String value = (String) Hibernate.STRING.nullSafeGet(resultSet, names[0]);
return ((value != null) ? new StringBuilder(value) : null);
}
public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index)
throws HibernateException, SQLException {
Hibernate.STRING.nullSafeSet(preparedStatement,
(value != null) ? value.toString() : null, index);
}
/ "Standard"-Implementierungen /
@Override
public boolean isMutable() {
return false;
}
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return ObjectUtils.equals(x, y);
}
@Override
public int hashCode(Object x) throws HibernateException {
assert (x != null);
return x.hashCode();
}
@Override
public Object deepCopy(Object value) throws HibernateException {
Rückgabewert;
}
@Override
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
das Original zurückgeben;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
zurückgeben;
}
}
[/java]
Abgesehen von den Null-Prüfungen, auf die man achten muss, ist das eine sehr einfache Sache. Und es funktioniert auch! Wir können einen StringBuilder aus der DB laden, und wenn wir einen neuen StringBuilder setzen, wird dieser korrekt persistiert. Bingo!
Nicht so schnell
...bis wir das Folgende1 ausprobieren, das heißt:
[java]
@Test
public void dehydrateModified() {
EntityWithStringBuilderProperty holder = loadEntity(EntityWithStringBuilderProperty.class, id);
StringBuilder builder = holder.getBuilder();
String addition = " Bond";
builder.append(addition);
session.flush();
session.evict(holder);
StringBuilder persistedBuilder =
((EntityWithStringBuilderProperty) loadEntity(EntityWithStringBuilderProperty.class, id))
.getBuilder();
assertNotSame(builder, persistedBuilder);
assertEquals(builder.toString(), persistedBuilder.toString());
}
[/java]
Peng! Der geladene Wert ist immer noch der ursprüngliche Wert, und die abschließende Assertion schlägt fehl. Aus der SQL-Datei geht hervor, dass Hibernate die Änderung aus irgendeinem Grund nicht erkannt hat:
Hibernate: select entitywith0_.id as id00, entitywith0_.builder as builder00 from EntityWithStringBuilderProperty entitywith0 where entitywith0.id=?
Hibernate: select entitywith0_.id as id00, entitywith0_.builder as builder00 from EntityWithStringBuilderProperty entitywith0 where entitywith0.id=?
Lesen, bevor Sie einfügen
Wenn wir uns unsere Copy'n'Paste-Implementierung noch einmal ansehen, scheint das Problem klar zu sein: Kein Wunder, dass Hibernate die Änderungen nicht aufnimmt, wenn wir den Typ als unveränderlich2 deklarieren! Nun, das lässt sich leicht beheben: [java] / "Vielleicht sollten wir sie veränderbar machen?" / @Override public boolean isMutable() { return true; } ... @Override public Object deepCopy(Object value) throws HibernateException { Rückgabewert; } / sollte gemäß der Dokumentation Kopien für veränderliche Daten zurückgeben / @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return deepCopy(Original); } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) deepCopy(Wert); } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return deepCopy(cached); } [/java]
me != me?
Netter Versuch, aber leider noch keine Zigarre: Hibernate erkennt immer noch nicht, dass die Eigenschaft geändert wurde. Aber warum? Hibernate sollte den Benutzertyp aufrufen, um festzustellen, ob das Objekt, das persistiert wird, gleich dem ursprünglich geladenen Objekt ist, und unsere Implementierung von equals(Object x, Object y) sollte sicherlich korrekt feststellen, dass das Objekt geändert wurde.
Das Setzen eines Haltepunkts für equals bestätigt in der Tat, dass es aufgerufen wird, was ist also los? Der Haltepunkt hilft, das Problem zu identifizieren:
Jetzt mit Kopieren
Für einen StringBuilder ist deepCopy trivial implementiert:
[java]
private static StringBuilder nullSafeToStringBuilder(Object value) {
return ((value != null) ? new StringBuilder(value.toString()) : null);
}
/ "Vielleicht wenn wir tatsächlich eine Kopie erstellen...?" /
@Override
public Object deepCopy(Object value) throws HibernateException {
return nullSafeToStringBuilder(value);
}
[/java]
Und siehe da, es funktioniert endlich wie erwartet:
Hibernate: select entitywith0_.id as id00, entitywith0_.builder as builder00 from EntityWithStringBuilderProperty entitywith0 where entitywith0.id=?
Hibernate: update EntityWithStringBuilderProperty set builder=? where id=?
Hibernate: select entitywith0_.id as id00, entitywith0_.builder as builder00 from EntityWithStringBuilderProperty entitywith0 where entitywith0.id=?
Verschwinden Sie, Kesselflicker!
So weit, so gut: Wir haben einen funktionierenden Benutzertyp. Aber wir haben auch eine enorme Menge an Boilerplate: für die fünf Methoden ( zurückgegebenKlasse, sqlTypes, nullSafeGet und
Wenn Gleiches mit Gleichem verwechselt wird
Eine Gleichheitsüberprüfung für einen StringBuilder mit vielen tausend Zeichen könnte eine teure Operation sein, die wir lieber vermeiden möchten. Es könnte zum Beispiel viel billiger sein, bei jeder Änderung des StringBuilders ein "Dirty"-Flag zu setzen und dann einfach zu prüfen, ob eine Aktualisierung notwendig ist. Die Basisklasse DirtyCheckableUserType ist für diese Art von Anwendungsfall gedacht. Hier ist ein (nicht sehr effizient implementierter) DirtyCheckingStringBuilder, den wir als Beispiel verwenden werden: [java] public class DirtyCheckingStringBuilder { private StringBuilder value; private boolean valueModified; ... public DirtyCheckingStringBuilder(String value) { assert (value != null); this.value = new StringBuilder(value); valueModified = false; } public DirtyCheckingStringBuilder append(String addition) { value.append(addition); valueModified = true; return this; } ... } [/java] Mit der Basisklasse DirtyCheckableUserType können wir dann einen Benutzertyp erstellen, der den Aufruf von equals bei "schmutzigen" Buildern6 vermeidet. [java] public class DirtyCheckingStringBuilderUserType extends DirtyCheckableUserType { ... @Override protected boolean isDirty(Object object) { return ((DirtyCheckingStringBuilder) object).wasModified(); [/java] } @Override public Object deepCopy(Object value) throws HibernateException { return nullSafeToStringBuilder(value); } } Normalerweise ist es natürlich wahrscheinlich, dass, wenn equals teuer ist, es auch die deepCopy sein wird, die die Kopie erzeugt hat, mit der verglichen werden soll. Mit der Dirty-Prüfung ist es nicht mehr notwendig, dass der von Hibernate aufgezeichnete geladene Zustand tatsächlich eine echte Kopie ist. Sie können also in Erwägung ziehen, einfach den Eingabewert in deepCopy zurückzugeben. [java] @Override public Object deepCopy(Object value) throws HibernateException { return value; } [/java] Da deepCopy jedoch auch von den Standardimplementierungen von assemble und anderen Methoden aufgerufen wird, kann dies zu Problemen führen, es sei denn, Sie sind sicher, dass Ihr Objekt nicht in den Cache serialisiert wird und Sie keine Merges durchführen. Eine mögliche Option wäre, die Implementierung dieser anderen Methoden zu überschreiben, um eine "echte" Kopie zurückzugeben7: [java] @Override public Object deepCopy(Object value) throws HibernateException { Rückgabewert; } @Override protected Object realDeepCopy(Object value) throws HibernateException { // Geben Sie hier eine tatsächliche tiefe Kopie zurück } public Object assemble(Serializable cached, Object owner) throws HibernateException { // auch sicher für veränderliche Objekte return realDeepCopy(cached); } public Serializable disassemble(Object value) throws HibernateException { // auch sicher für veränderliche Objekte Object deepCopy = realDeepCopy(Wert); assert (deepCopy instanceof Serializable); return (Serializable) deepCopy; } public Object replace(Object original, Object target, Object owner) throws HibernateException { // auch sicher für veränderliche Objekte return realDeepCopy(Original); } [/java]
- Der Beispiel-Quellcode ist bei Google Code verfügbar.
- Wenn Sie die Eigenschaft auf ein anderes Objekt setzen , wird dies natürlich von Hibernate übernommen.
- Für Neugierige: Dies wird in der Eigenschaft loadedState des EntityEntry für die Entität festgehalten. Diese wiederum werden in der entityEntries-Map der Eigenschaft persistenceContext der Sitzung gespeichert.
- AbstractSaveEventListener:303
- Wenn Sie Maven verwenden, müssen Sie Folgendes zu Ihrem POM hinzufügen:
<Abhängigkeit> <groupId>com.qrmedia.commons</groupId> <artifactId>commons-hibernate-usertype</artifactId> <Version>1.0-SNAPSHOT</version> </dependency> ... <Repository> <id>qrmedia-snapshots</id> <url>https://aphillips.googlecode.com/svn/maven-repository/snapshots</url> </repository>
- DirtyCheckableUserType führt auch dann eine Gleichheitsprüfung durch, wenn beide Objekte, die an equals(Object x, Object y) übergeben werden, sauber sind. Denn nur weil sie sauber sind, heißt das noch lange nicht, dass sie auch gleich sind (obwohl das für ein Paar "frische Kopie"/"aktuelles Objekt" gilt).
- Vorbehalt: Ich habe das nicht ausprobiert. Wenn Sie bereits einen Grund kennen, warum es nicht funktionieren könnte, schreiben Sie bitte einen Kommentar!
Verfasst von
Andrew Phillips
Unsere Ideen
Weitere Blogs
Contact



