/ CONVERT, CUSTOM, DATABASE, FACTS, HIBERNATE, MAPPING, OBJECT, TYPE, USERTYPE

Hibernate hard facts part 3

In this week’s article, 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.

This is the 3rd post in the Hibernate hard facts focus series.Other posts include:

  1. Hibernate hard facts part 1
  2. Hibernate hard facts part 2
  3. Hibernate hard facts part 3 (this post)
  4. Hibernate hard facts - Part 4
  5. Hibernate hard facts – Part 5
  6. Hibernate hard facts - Part 6
  7. Hibernate hard facts – Part 7

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

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Hibernate hard facts part 3
Share this