Thursday, January 20, 2011

Better Enum Mapping with Hibernate

JPA and Hibernate provides two ways to map enum fields to database fields: either map the enum value ordinal with @Enumerated(EnumType.ORDINAL) or the enum value name with @Enumerated(EnumType.STRING) . Both cases are not ideal, because it's very easy to make changes to the enum, such as changing values order or renaming values, and forgetting to migrate the existing values in the database accordingly. Actually the need to migrate existing data is not justified if you just want to change Java code.

Here is a mechanism that lets you decouple the enum value names and ordinals from the data, while still making it easy to map enum fields to database columns.

The solution can be specific to an enum type, but it would be better to have all your persistent enum types implement the same interface to reuse the mechanism. Let's define the PersistentEnum interface:

public interface PersistentEnum {
    int getId();
}

and it will be implemented by enum types, for example:

public enum Gender implements PersistentEnum {

    MALE(0),
    FEMALE(1);

    private final int id;

    Gender(int id) {
        this.id = id
    }

    @Override
    public int getId() {
        return id;
    }

}

Now we need to define the Hibernate User Type that will do the conversion both ways. Each enum requires its own user type, so first we will code an abstract superclass that works with the PersistentEnum interface and then subclass it for each enum:

public abstract class PersistentEnumUserType<T extends PersistentEnum> implements UserType {

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

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

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

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return x == y;
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return x == null ? 0 : x.hashCode();
    }

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

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
            throws HibernateException, SQLException {
        int id = rs.getInt(names[0]);
        if(rs.wasNull()) {
            return null;
        }
        for(PersistentEnum value : returnedClass().getEnumConstants()) {
            if(id == value.getId()) {
                return value;
            }
        }
        throw new IllegalStateException("Unknown " + returnedClass().getSimpleName() + " id");
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index)
            throws HibernateException, SQLException {
        if (value == null) {
            st.setNull(index, Types.INTEGER);
        } else {
            st.setInt(index, ((PersistentEnum)value).getId());
        }
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        return original;
    }

    @Override
    public abstract Class<T> returnedClass();

    @Override
    public int[] sqlTypes() {
        return new int[]{Types.INTEGER};
    }

}

The interesting methods are nullSafeGet() which is called when the resultset from the database is mapped to an object, and nullSafeSet() which is called when the fields of an object are mapped to SQL parameters of insert/update/delete statements.

The extension point is the abstract method returnedClass() - in every subclass we will override it so it returns the specific enum class. A User Type for the Gender enum defined above would like this:

public class GenderUserType extends PersistentEnumUserType<Gender> {

    @Override
    public Class<Gender> returnedClass() {
        return Gender.class;
    }

}

The last thing to do is to configure fields of enum types to use the appropriate user types:

@Entity
public class Person {

    @Type(type="it.recompile.GenderUserType")
    private Gender gender;

    // other fields and methods...
}

Note that the type attribute should contain the fully qualified class of the user type, including the package.

That's it - now you can safely make changes to your enum classes without worrying about problems with existing data, as long as you don't change the id values in the constructors (which you have no reason to do).

9 comments:

  1. Hello,

    Just as an addendum:

    The code needs a slight update in order to support sql-enum-parsing like

    SELECT COUNT(p) FROM ProfileData p WHERE p.state = de.arctis.demo.ProfileData$StateType.aktive ... "

    I found the solution in an old discussion on https://forum.hibernate.org/viewtopic.php?t=930539

    public abstract class PersistentEnumUserType implements EnhancedUserType {

    ...

    public String objectToSQLString(Object obj) {
    if(obj == null) return "null";
    if (obj instanceof PersistentEnum) {
    PersistentEnum enumObject = (PersistentEnum) obj;
    return "" + enumObject.getId();
    }
    throw new IllegalArgumentException("Cannot convert " + obj.getClass());
    }
    public String toXMLString(Object obj) {
    return objectToSQLString(obj);
    }
    public Object fromXMLString(String s) {
    if(StringUtils.isEmpty(s) || s.trim().equalsIgnoreCase("null")) return null;
    int id = Integer.parseInt(s);
    for(PersistentEnum value : returnedClass().getEnumConstants()) {
    if(id == value.getId()) {
    return value;
    }
    }
    throw new IllegalArgumentException("Cannot parse XML " + s + " to " + this.returnedClass().getName());

    }


    Not sure about the to/fromXMLString, but objectToSQLString is important.
    (sorry for the bad formatting)

    ReplyDelete
  2. Thanks very much. I searched many websites and blogs for this solution without any progress. But, your tutorial helped me. Thanks!!

    ReplyDelete
  3. I need to create a custom type for each of the enum, right ? Is there any way to avoid that ?

    ReplyDelete
    Replies
    1. By definition a user type is specific to a type (specific enum in our case) because it needs to know how to instantiate it in nullSafeGet()

      Delete
    2. Hmm. In my case, I have around 20 enums already and have scope to increase that number. So, to save the data using hibernate, I need to write 20+ custom types for enums, right ? Is that the way? Or am I using any wrong approach ?

      Delete
    3. You have options - the approach above, Hibernates build in mapping by order or name, code generation of user types for each enum, or write a framework that integrates with the Hibernate metamodel (low level advanced stuff).

      Delete
    4. Thanks for the reply. I can't use the ordinal/name mapping anyway. Let me check which one is better from other options.

      Delete
  4. For memory saving, I recommend to use SMALLINT type instead of INTEGER. I suppose, that you have less than 32767 enums types ;)

    ReplyDelete