SQL Zone is brought to you in partnership with:

Nicolas Frankel is an IT consultant with 10 years experience in Java / JEE environments. He likes his job so much he writes technical articles on his blog and reviews technical books in his spare time. He also tries to find other geeks like him in universities, as a part-time lecturer. Nicolas is a DZone MVB and is not an employee of DZone and has posted 224 posts at DZone. You can read more from them at their website. View Full User Profile

Hibernate Hard Facts

12.14.2009
| 7871 views |
  • submit to reddit

In the third article of this series, I will show how to tweak Hibernate so as to convert any database data types to and from any Java type and thus decouple your database model from your object model.

Custom type mapping

Hibernate is a very powerful asset in any application needing to persist data. As an example, I was tasked this week to generate the Object-Oriented model for a legacy database. It seemed simple enough, at first glance. Then I discovered a big legacy design flaw: for historical reasons, dates were stored as number in the YYYYMMDD format. For example, 11th december 2009 was 20091211. I couldn’t or rather wouldn’t change the database and yet, I didn’t want to pollute my neat little OO model with Integer instead of java.util.Date.

After browsing through Hibernate documentation, I was confident it made this possible in a very simple way.

Creating a custom type mapper

The first step, that is also the biggest, consist in creating a custom type. This type is not a real “type” but a mapper that knows how to convert from the database type to the Java type and vice-versa. In order to do so, all you have is create a class that implements org.hibernate.usertype.UserType. Let’s have a look at each implemented method in detail.

The following method gives away what class will be returned at the end of read process. Since I want a Date instead of an Integer, I naturally return the Date class.

public Class returnedClass() {

return Date.class;
}

The next method returns what types (in the Types constants) the column(s) that will be read fromhave. It is interesting to note that Hibernate let you map more than one column, thus having the same feature as the JPA @Embedded annotation. In my case, I read from a single numeric column, so I should return a single object array filled with Types.INTEGER.

public int[] sqlTypes() {

return new int[] {Types.INTEGER};

}

This method will check whether returned class instances are immutable (like any normal Java types save primitive types and Strings) or mutable (like the rest). This is very important because if false is returned, the field using this custom type won’t be checked to see whether an update should be performed or not. It will be of course if the field is replaced, in all cases (mutable or immutable). Though there’s is a big controversy in the Java API, the Date is mutable, so the method should return true.

 public boolean isMutable() {
return true;
}

I can’t guess how the following method is used but the API states:

Return a deep copy of the persistent state, stopping at entities and at collections. It is not necessary to copy immutable objects, or null values, in which case it is safe to simply return the argument.

Since we just said Date instances were mutable, we cannot just return the object but we have to return a clone instead: that’s made possible because Date’s clone() method is public.

public Object deepCopy(Object value) throws HibernateException {

return ((Date) value).clone();
}

The next two methods do the real work to respectively read from and to the database. Notice how the API exposes ResultSet object to read from and PreparedStatement object to write to.

public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {

Date result = null;

if (!rs.wasNull()) {

Integer integer = rs.getInt(names[0]);

if (integer != null) {

try {

result = new SimpleDateFormat("yyyyMMdd").parse(String.valueOf(integer));

} catch (ParseException e) {

throw new HibernateException(e);
}
}
}

return result;
}

public void nullSafeSet(PreparedStatement statement, Object value, int index) throws HibernateException, SQLException {

if (value == null) {

statement.setNull(index, Types.INTEGER);

} else {

Integer integer = Integer.valueOf(new SimpleDateFormat("yyyyMMdd").format((String) value));

statement.setInt(index, integer);
}
}

The next two methods are implementations of equals() and hasCode() from a persistence point-of-view.

public int hashCode(Object x) throws HibernateException {

return x == null ? 0 : x.hashCode();
}

public boolean equals(Object x, Object y) throws HibernateException {

if (x == null) {

return y == null;
}

return x.equals(y);
}

For equals(), since Date is mutable, we couldn’t just check for object equality since the same object could have been changed.

The replace() method is used for merging purpose. It couldn’t be simpler.

public Object replace(Object original, Object target, Object owner) throws HibernateException {

  Owner o = (Owner) owner;

  o.setDate(target);

  return ((Date) original).clone();
}

My implementation of the replace() method is not reusable: both the owning type and the name of the setter method should be known, making reusing the custom type a bit hard. If I wished to reuse it, the method’s body would need to use the lang.reflect package and to make guesses about the method names used. Thus, the algorithm for creating a reusable user type would be along the lines of:

  1. list all the methods that are setter and take the target class as an argument
    1. if no method matches, throw an error
    2. if a single method matches, call it with the target argument
    3. if more than one method matches, call the associated getter to check which one returns the original object

The next two methods are used in the caching process, respectively in serialization and deserialization. Since Date instances are serializable, it is easy to implement them.

public Serializable disassemble(Object value) throws HibernateException

return (Date) ((Date) value).clone();
}

public Object assemble(Serializable cached, Object owner) throws HibernateException {

return ((Date) cached).clone();
}

Declare the type on entity

Once the custom UserType is implemented, you need to make it accessible it for the entity.

@TypeDef(name="dateInt", typeClass = DateIntegerType.class)
public class Owner {

...
}

Use the type

The last step is to annotate the field.

@TypeDef(name="dateInt", typeClass = DateIntegerType.class)

public class Owner {

private Date date;

@Type(type="dateInt")
public Date getDate() {

return date;
}

public void setDate(Date date) {

this.date = date;
}
}

You can download the sources for this article here.

To go further:

From http://blog.frankel.ch
Published at DZone with permission of Nicolas Frankel, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Gary Hirschhorn replied on Thu, 2010/02/18 - 6:23pm

Thanks for the great article.  We sometimes store our Java Dates as database type Long in order to keep the millisecond precision (MySql native date type only has precision to the second), so we modified the class appropriately and it worked great.

We found one error though.  In nullSafeGet(), you make the call to ResultSet.wasNull() before reading the column value. However, the JavaDoc API for wasNull() specifies "Note that you must first call one of the getXXX methods on a column to try to read its value and then call the method wasNull to see if the value read was SQL NULL."  In our case, our column allows nulls but we were getting Longs of 0 instead.  Simply moving the rs.getXXX() call before the wasNull() call solved the problem.

Thanks again for a great article.

Nicolas Frankel replied on Fri, 2010/02/19 - 2:32am in response to: Gary Hirschhorn

Thanks for the comment Gary. The pitfall you mention is worth it!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.