In the careers of most Spring/Hibernate developers I know, there sooner or later comes a point of no escape...they have to write a Hibernate user type. The first one is usually of the copy'n'paste variety, and by and large works perfectly well.
But when things are no longer going quite as expected - Hibernate is ignoring changes to items managed by the user type, for instance - it often becomes apparent that one doesn't sufficiently understand how these user type thingies are supposed to work. At least, that's what happened to me.
In this post, we'll be dissecting the Hibernate
UserType interface, explaining the relationships between the various methods, and developing a set of base user types that capture common use cases.
Hibernate's UserType interface, in all it's glory (we'll be leaving the more complex
EnhancedUserType to one side for the moment), looks like this:
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;
}
Of the methods, returnedClass, nullSafeSet and nullSafeGet are probably the most self-explanatory. They are likely also the ones you tweaked for your first user type. Indeed, they are perhaps the
only ones one would really want to have to deal with as a user type implementor, and making this (almost) possible is one of the main purposes of the base classes we will develop.
What about the other methods, assemble, replace and the like? These used to be the ones whose implementation one copied from the example in semi-blind faith, although now that the
Javadoc for them is much more detailed they will hopefully be more straightforward to implement. Still, the following "default" implementation is still common:
@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 {
return value;
}
@Override
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
return original;
}
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
@Override
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
return cached;
}
What's wrong with this? Apart from the clutter, nothing, really...as long as you intend your objects to be treated as
immutable.
The following
blog post describes problems that can arise with this implementation, and builds up base classes that allow you to focus, as much as possible, on only writing code relevant to the actual target type. You should end up with something like:
public class ReadableStringBuilderUserType extends MutableUserType {
public Class returnedClass() {
return StringBuilder.class;
}
public int[] sqlTypes() {
return new int[] { Types.VARCHAR };
}
public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)
throws HibernateException, SQLException {
return nullSafeToStringBuilder(Hibernate.STRING.nullSafeGet(resultSet, names[0]));
}
private static StringBuilder nullSafeToStringBuilder(Object value) {
return ((value != null) ? new StringBuilder(value.toString()) : null);
}
public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index)
throws HibernateException, SQLException {
Hibernate.STRING.nullSafeSet(preparedStatement,
(value != null) ? value.toString() : null, index);
}
@Override
public Object deepCopy(Object value) throws HibernateException {
return nullSafeToStringBuilder(value);
}
}