Understanding and writing Hibernate custom user types

Andrew Phillips

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!

Comments (20)

  1. Tom - Reply

    November 10, 2009 at 1:36 am

    I know Hibernate and ActiveRecord really well, and I understand their complexity. Articles like this highlight them.

    I expect the noSQL movement to make real traction because of this complexity.

  2. Attila - Reply

    January 5, 2010 at 5:27 pm

    Thanks for the tutorial.

    I'm having problems defining the mapping required to use UserType as a composite element when trying to persist the element within a MAP. My mapping files has the following code:

    Every time I try to run the code Hibernate keeps looking for the setter/getter for the component properties in the AttributeUserType which ofcourse do not exists as AttributeUserType is a class that extends UserType (your equivalent to your ReadableStringBuilderUserType).

    I hope you could help with this issue.

  3. Andrew Phillips - Reply

    January 7, 2010 at 11:34 am

    @Attila: Seems the mapping file you tried to submit was filtered out. Could you paste it to ideone.com or so? I can then try to include it in your comment.

  4. Miguel Cohnen - Reply

    February 8, 2010 at 1:21 pm

    Hi, thank you for this great post, it really helped a lot. But I'm finding a strange problem. I'm using Seam Framework, and inside my nullSafeSet I need to do some queries to the database. To do so, I grab my entityManager using Component.getInstance("entityManager"). This works fine if What I'm doing is merging an existent entity. But If I create a new one using persist, and I use the entityManager inside nullSafeSet method, I always get this error:
    ERROR [AssertionFailure] an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
    org.hibernate.AssertionFailure: null id in com.amadeus.websolutions.irmap.model.Airport entry (don't flush the Session after an exception occurs)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:55)

    I know it has to be something related to hibernate sessions, but I can not imagine what...

    Can you provide some help here?

    Thank you!

  5. Andrew Phillips - Reply

    February 16, 2010 at 1:20 pm

    @Miguel: getting an entity manager that may or may not be using the same session from inside a session persistence operation like nullSafeSet is indeed likely to get you into trouble.
    Without a more detailed picture of how Hibernate is set up in your case it's difficult to guess what precisely might be wrong. You might have more luck using CompositeUserType, whose nullSafeSet method takes an additional session parameter which you can use for your queries.

    Good luck!

  6. Prashant - Reply

    February 22, 2010 at 2:13 pm

    I have a couple of table each of which are having version column. These tables are populated with data load from flat files. Flat files data load does not populate version column. Don't know how to populate version column value. I have however 2 cases:

    Case 1:

    version property as nullable and setting default value as 1. After data migration value becomes as null (dont know why DB2 ignores default value).

    Case 2:

    version property as not nullable. In this case null pointer exception is coming as migrated data does not contain version value.

    We are having DB2 database and don't want to change flat files also (I don't want any kind of trigger solution).

    Is Version UserType of any help here?

  7. Andrew Phillips - Reply

    February 23, 2010 at 4:43 pm

    @Prashant: I assume "data load from flat files" means you're importing the data without using Hibernate, i.e. via the DB2 or some other SQL client.
    If you're having problems populating the version column at this stage the most advisable thing would seem to be to investigate why "DB2 ignores the default value" and see if you can either fix that or run some cleanup SQL after the import (a trigger would be another obvious option but you mentioned that you wanted to avoid that).

    You could try using nullSafeGet in a UserVersionType to work around this problem, but even though I think it could work I wouldn't recommend it. For one, you're changing your application code to deal with a data problem that only occurs during import, not normal application operation. This way, you risk missing real data problems unless you find a way to change the nullSafeGet behaviour at runtime.

    You are also going to have to remove the non-nullable constraint on the column, which again weakens your defences against data corruption. Hibernate can't help you there (again, I'm assuming here that your flat file import is going straight to the DB, not via Hibernate).

  8. bhargava - Reply

    January 26, 2011 at 1:23 am

    Thank you for the nice post! It really helped!

  9. Sanjeev - Reply

    June 4, 2011 at 8:03 pm

    Thanks I am new to hibernate and Active record. I really appreciate this post. Thanks for the tutorial.

  10. Tanja - Reply

    June 14, 2011 at 3:20 pm

    Thank you very much!
    Made my work really easy :)

  11. rocks - Reply

    October 20, 2011 at 2:19 pm

    Hi,

    We have used one user type in our porject for String DataType.
    e.g. CustomString.java, CustomStringUserType.java.
    now the problem i am facing is that whenevr we dont have any value it passes blank to the database causing loss of data save. I think probably the below line is cauisng that problem in my else conditon of userType class. we have similar user types for Date, Integer tc.. could you pls advise

    nullSafeSet(preparedStatement,Types.VARCHAR);

  12. Andrew Phillips - Reply

    October 21, 2011 at 3:22 pm

    @rocks: Thanks for your question! From what you say it would indeed seem that the behaviour you're observing could be caused by your nullSafeSet implementation, but I would need some more information to help further:

    * which of the user types are you extending: MutableUserType? ImmutableUserType?
    * what does your nullSafeSet implementation look like (if you can, please post a copy on Pastebin or similar)?
    * what is the desired behaviour for your application if the value is empty?

  13. rocks - Reply

    October 21, 2011 at 4:10 pm

    hi Andrew,

    its actully not updating the value in column whereas the changed value is in the entity attribute.. our nulTosafeget method is same.. we havnet done anything in that.. and it returning 'false' for isMutable.. the datatype of usertype is date.. based on soame columns which are again Usertype of stirng data type...

  14. rocks - Reply

    October 21, 2011 at 6:54 pm

    Hi Andrew,

    Let me explain.
    I have value 123 in DB..
    And after some opertaion i chnaged the value in entity bean to 457.. but when i persist it in DB somehow this column value is not getting modified.. for other it works well..

    I am using StringUserType.. can u sugegst where should i put a check so that it can see that the value need to be is updated..

  15. Andrew Phillips - Reply

    October 21, 2011 at 7:13 pm

    @rocks: Thanks for the additional information. If I understand you correctly, you're talking about a plain String property?
    In that case, Hibernate should be able to persist it (and handle the updating etc.) by default - why are you using a UserType?

    Also, the commons-hibernate-usertype project doesn't include a StringUserType, so I'd need to see the actual code to be able to help you more.

  16. [...] Here is a custom Hibernate UserType that will work to persist Java array types (string[], int[], boolean[], etc..) in a single column. I have seen Oracle implementations, but here is one that uses Java.sql.array and works for Postgres. If you’d like to know more about how this works, Andrew Phillips has written a great explanation of custom Hibernate UserTypes. [...]

  17. Troy Tolle - Reply

    October 3, 2012 at 2:22 am

    Great detail to this with wonderful explanation. Was happy and relieved to find that someone had actually solved this problem and posted about it. Thanks for sharing, it made my development much easier.

  18. Olja - Reply

    March 13, 2013 at 8:33 pm

    Great article! Once the user type is defined, is there a way to automatically have it used for an entity property of a certain type, thus not requiring the annotation @Type(name-"ReadableStringBuilderUserType") to be specified each time?

    Thanks again!

  19. Andrew Phillips - Reply

    March 14, 2013 at 4:37 am

    @Olja: If I'm understanding you correctly and you're trying to say "for every property of Java class/type X, use this Hibernate user type", it looks like the type registry introduced in 3.6.0 might be what you're after.

    See also the (admittedly quite short) type registry section in the Hibernate documentation and the following Stack Overflow question.

Add a Comment