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.

The naked truth

Hibernate's UserType interface, in all it's glory (we'll be leaving the more complex CompositeUserType 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.

Building up a case

Right, time to leave these heady abstract hights and cook up the usual artificial example. Imagine, thus, that you want to persist an entity with a StringBuilder property for, oh, I don't know, storing a frequently-updated history, for instance.
Now, StringBuilder is, of course, serializable, so can be handled by JPA (and thus Hibernate) out of the box. But what you get in the database isn't incredibly practical: a value of "Agent 007 entered the secret hideout" might end up as

aced0005737200176a6176612e6c616e672e537472696e674275696c64
65723cd5fb145a4c6acb0300007870770400000024757200025b43b0266
6b0e25d84ac020000787000000024004100670065006e0074002000300
030003700200065006e007400650072006500640020007400680065002
000730065006300720065007400200068006900640065006f0075007478

Useful, eh?

Instead, we'll do the obvious thing and persist the builder's string representation. Following the trusty copy'n'paste approach outlined above, we end up with:

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);
    }

    /* "default" implementations */
    
    @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;
    }

}

Apart from the null checks that one has to watch out for, very straightforward stuff. And it works, too! We can load a StringBuilder from the DB, and if we set a new StringBuilder it is correctly persisted. Bingo!

Not so fast

...until we try the following1, that is:

@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());
}

Bang! The value loaded is still the original value, and the final assertion fails. From the SQL it looks like Hibernate hasn't picked up the modification, for some reason:

Hibernate: select entitywith0_.id as id0_0_, entitywith0_.builder as builder0_0_ from EntityWithStringBuilderProperty entitywith0_ where entitywith0_.id=?
Hibernate: select entitywith0_.id as id0_0_, entitywith0_.builder as builder0_0_ from EntityWithStringBuilderProperty entitywith0_ where entitywith0_.id=?

Read before you paste

Looking again at our copy'n'paste implementation, the problem seems clear: no wonder Hibernate isn't picking up changes if we declare the type to be immutable2! Well, that's easily fixed:

/* "Maybe if we make it mutable..?" */

@Override
public boolean isMutable() {
    return true;
}

...

@Override
public Object deepCopy(Object value) throws HibernateException {
    return value;
}

/* should return copies for mutables, according to the documentation */

@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(value);
}

@Override
public Object assemble(Serializable cached, Object owner)
        throws HibernateException {
    return deepCopy(cached);
}

me != me?

Nice try, but unfortunately no cigar yet: Hibernate still fails to realise that the property has been modified. But why? Hibernate should call the user type to determine if the object being persisted is equal to the one originally loaded, and our implementation of equals(Object x, Object y) should surely correctly determine that the object has been modified.

Indeed, setting a breakpoint on equals confirms that it is called, so what is going on? The breakpoint helps identify the problem: equals is being called with both arguments being references to the same, modified, StringBuilder object! So of course equals doesn't detect the change - it would be a strange equals indeed that obeyed equals(x, x) == false.

It appears, therefore, that Hibernate is "losing track" of the originally loaded state3 of the object, which makes it impossible for the user type to detect the change. A bug in Hibernate?

No. Digging a little deeper4 into the Hibernate code, it turns out that Hibernate's snapshot of the loaded state of an object is generated by a call to the user type's deepCopy method...from which our user type is simply returning a reference to the loaded object!

Without a clean copy of the loaded state (Hibernate's "clean" copy is being updated whenever we update the loaded object), it's no surprise that Hibernate can't detect that the object has been modified.

Now with copying

For a StringBuilder, deepCopy is trivially implemented:

private static StringBuilder nullSafeToStringBuilder(Object value) {
    return ((value != null) ? new StringBuilder(value.toString()) : null);
}

/* "Maybe if we make actually create a copy..?" */

@Override
public Object deepCopy(Object value) throws HibernateException {
    return nullSafeToStringBuilder(value);
}

And lo and behold, this finally does work as expected:

Hibernate: select entitywith0_.id as id0_0_, entitywith0_.builder as builder0_0_ from EntityWithStringBuilderProperty entitywith0_ where entitywith0_.id=?
Hibernate: update EntityWithStringBuilderProperty set builder=? where id=?
Hibernate: select entitywith0_.id as id0_0_, entitywith0_.builder as builder0_0_ from EntityWithStringBuilderProperty entitywith0_ where entitywith0_.id=?

Be gone, boilerplate!

So far, so good: we have a working user type. But we also have a tremendous amount of boilerplate: for the five methods (returnedClass, sqlTypes, nullSafeGet and -Set and deepCopy) containing six lines of code that actually deal with StringBuilders there are six methods dealing with "generic" functionality.

The first step in our quest to simplify our user type is, therefore, to extract the generic stuff into a base class:

public class ReadableStringBuilderUserType extends MutableUserType {

    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 {
        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);
    }

}

Extending from MutableUserType removes the boilerplate from our user type, leaving only StringBuilder-related code. Nice!

commons-hibernate-usertype5 also contains, amongst others, ImmutableUserType and XStreamableUserType, which provides convenient (and readable!) Object-to-XML serialization.

When equals equals trouble

Performing an equals check on a StringBuilder of many thousands of characters could be an expensive operation that we'd rather avoid. It might be much cheaper, for instance, to set a "dirty" flag whenever the StringBuilder is modified and then simply check this when deciding if an update is necessary. The DirtyCheckableUserType base class is intended for this kind of use case.

Here's a (not very efficiently implemented) DirtyCheckingStringBuilder we'll use as an example:

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;
    }
    
    ...
}

Using the DirtyCheckableUserType base class, we can then create a user type that will avoid calling equals on "dirty" builders6.

public class DirtyCheckingStringBuilderUserType extends DirtyCheckableUserType {
    ...
        
    @Override
    protected boolean isDirty(Object object) {
        return ((DirtyCheckingStringBuilder) object).wasModified();
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return nullSafeToStringBuilder(value);
    }

}

Usually, of course, it is likely that, if equals is expensive, then so will the deepCopy be that produced the copy to be compared against in the first place. With dirty checking, it is no longer necessary for the loaded state recorded by Hibernate to actually be a genuine copy, so you may consider simply returning the input value in deepCopy.

@Override
public Object deepCopy(Object value) throws HibernateException {
    return value;
}

However, since deepCopy is also called by the default implementations of assemble and other methods, this is likely to cause problems unless you're certain that your object will not be serialized to cache and you will not be performing merges. One possible option would be to override the implementation of these other methods to return a "real" copy7:

@Override
public Object deepCopy(Object value) throws HibernateException {
    return value;
}

@Override
protected Object realDeepCopy(Object value) throws HibernateException {
    // return an actual deep copy here
}

public Object assemble(Serializable cached, Object owner)
        throws HibernateException {
    // also safe for mutable objects
    return realDeepCopy(cached);
}

public Serializable disassemble(Object value) throws HibernateException {
    // also safe for mutable objects
    Object deepCopy = realDeepCopy(value);
    assert (deepCopy instanceof Serializable);
    return (Serializable) deepCopy;
}

public Object replace(Object original, Object target, Object owner)
        throws HibernateException {
    // also safe for mutable objects
    return realDeepCopy(original);
}
Footnotes

  1. Example source code is available at Google Code.
  2. Setting the property to a different object is picked up by Hibernate, of course.
  3. For the curious, this is held by the loadedState property of the EntityEntry for the entity. These in turn are held in the entityEntries map of the session's persistenceContext property.
  4. AbstractSaveEventListener:303
  5. You'll need to add the following to your POM if using Maven:
    <dependency>
      <groupId>com.qrmedia.commons</groupId>
      <artifactId>commons-hibernate-usertype</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    ...
    <repository>
      <id>qrmedia-snapshots</id>
      <url>http://aphillips.googlecode.com/svn/maven-repository/snapshots</url>
    </repository>
    
  6. DirtyCheckableUserType will still perform an equals check if both object passed to equals(Object x, Object y) are clean: after all, just because they are clean it does not follow, in general, that they are equal (although that does hold for a "fresh copy"/"current object" pair).
  7. Caveat: I haven't tried this. If you already know of some reason why this might not work please post a comment!