Back

UserEnumTypeMapping

Comments

The previous pages described experiments with toy table implementations using Spring JDBC. This page describes a first implementation of the implementation of a real table with Hibernate. Of course the first problem I ran into was that Hibernate has no support for Enum types (yet?). You can use them, but the whole Enum type is serialized and stuffed in the table field. Effective Java stated that you should avoid this type of serialization, so an alternative is needed. The bright minds at the Hibernate forum found a solution.

Evaluation

All the enums now use Strings as database values. The methods identifier() and fromString() are called from the EnumUserType class. The Map, static block and the fromString() replace the dynamic lookup with a map search initialised at class loading:

package org.electrickery.jscbook.enums;

import java.util.HashMap;
import java.util.Map;

public enum Gender {
	FEMALE("0"), MALE("1"), 
	NEUTERED_MALE("2"), NEUTERED_FEMALE("3"), 
	ABNORMAL("4"), UNKNOWN("5"),
	CONTRACEPTED_MALE("6"), CONTRACEPTED_FEMALE("7");
	
	private String genderCode;
	
	private Gender(String code) {
		genderCode = code;
	}

	// for the fromString method to work (Effective Java, 2nd Ed., p.154)
	private static final Map<String, Gender> stringToEnum = new HashMap<String, Gender>();
	
	static {
		for (Gender g : values()) {
			stringToEnum.put(g.identifier(), g);
		}
	}
	
	// used by EnumUserType via Master.hbm.xml
	public String identifier() {
		return genderCode;
	}
	
	public static Gender fromString(String code) {
		return stringToEnum.get(code);
	}

}

Usage of the valueOf() method didn't work when using Hibernate persistence (more on this later). It complained that it was already 'implicit' implemented, but using this 'implicit' method generated runtime errors in not finding the Enum instances (at least for the String based Enums):


<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.electrickery.jscbook.tableMapping">

	<typedef name="birthType" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.master.BirthType</param>
		<param name="identifierMethod">identifier</param>
		<param name="valueOfMethod">fromString</param>
	</typedef>
	<typedef name="dateEst" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.enummap.DateEst</param>
		<param name="identifierMethod">identifier</param>
		<param name="valueOfMethod">fromString</param>
	</typedef>
	<typedef name="gender" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.master.Gender</param>
		<param name="identifierMethod">identifier</param>
		<param name="valueOfMethod">fromString</param>
	</typedef>
	<typedef name="rearing" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.master.Rearing</param>
		<param name="identifierMethod">identifier</param>
		<param name="valueOfMethod">fromString</param>
	</typedef>
	
	<class name="org.electrickery.jscbook.master.Master" table="MASTER">
		<id name="id" type="long" column="ID">
			<generator class="identity" />
		</id>
		<property name="bookId" type="long" column="BOOK_ID" />
		<property name="studId" type="string">
			<column name="STUD_ID" length="6" not-null="true" />
		</property>
		<property name="gender" type="gender">
			<column name="GENDER" not-null="true" length="1" />
		</property>
		<property name="hybrid" type="boolean" column="HYBRID" />
		<property name="damId" type="string">
			<column name="DAM_ID" length="6" not-null="true" />
		</property>
		<property name="sireId" type="string">
			<column name="SIRE_ID" length="6" not-null="true" />
		</property>
		<property name="birthType" type="birthType">
			<column name="BIRTH_TYPE" length="1" />
		</property>
		<property name="birthDate" type="java.util.Date" column="BIRTH_DATE"
			not-null="true" />
		<property name="birthDateEst" type="dateEst">
			<column name="BIRTH_DATE_EST" not-null="true" length="1" />
		</property>
		<property name="rearing" type="rearing">
			<column name="REARING" length="1" />
		</property>
	</class>
	
</hibernate-mapping>

This bit is copied from the Hibernate site. Quite a lot of code to convert simple Enums, but it does work!

package org.electrickery.jscbook.enummap;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.type.NullableType;
import org.hibernate.type.TypeFactory;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;

/**
 * 
 * @author cplehew, from http://www.hibernate.org/272.html, equal to the GenericEnumUserType class 
 * in "Flexible solution - working version"
 * Apparently all this code is needed to convert a enum to a simple value in the table and back.
 *
 */

public class EnumUserType implements UserType, ParameterizedType {
    private static final String DEFAULT_IDENTIFIER_METHOD_NAME = "identifier";
    private static final String DEFAULT_VALUE_OF_METHOD_NAME = "fromString";

    private Class enumClass;
    private Class identifierType;
    private Method identifierMethod;
    private Method valueOfMethod;
    private NullableType type;
    private int[] sqlTypes;

    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enumClassName");
        try {
            enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
        } catch (ClassNotFoundException cfne) {
            throw new HibernateException("Enum class not found", cfne);
        }

        String identifierMethodName = parameters.getProperty("identifierMethod", DEFAULT_IDENTIFIER_METHOD_NAME);

        try {
            identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
            identifierType = identifierMethod.getReturnType();
        } catch (Exception e) {
            throw new HibernateException("Failed to obtain identifier method", e);
        }

        type = (NullableType) TypeFactory.basic(identifierType.getName());

        if (type == null)
            throw new HibernateException("Unsupported identifier type " + identifierType.getName());

        sqlTypes = new int[] { type.sqlType() };

        String valueOfMethodName = parameters.getProperty("valueOfMethod", DEFAULT_VALUE_OF_METHOD_NAME);

        try {
            valueOfMethod = enumClass.getMethod(valueOfMethodName, new Class[] { identifierType });
        } catch (Exception e) {
            throw new HibernateException("Failed to obtain valueOf method", e);
        }
    }

    public Class returnedClass() {
        return enumClass;
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {  
        Object identifier = type.get(rs, names[0]);
        if (rs.wasNull()) {
            return null;
        }
        
        try {
            return valueOfMethod.invoke(enumClass, new Object[] { identifier });
        } catch (Exception e) {
            throw new HibernateException("Exception while invoking valueOf method '" + valueOfMethod.getName() + "' of " +
                    "enumeration class '" + enumClass + "'", e);
        }
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
        try {
            if (value == null) {
                st.setNull(index, type.sqlType());
            } else {
                Object identifier = identifierMethod.invoke(value, new Object[0]);
                type.set(st, identifier, index);
            }
        } catch (Exception e) {
            throw new HibernateException("Exception while invoking identifierMethod '" + identifierMethod.getName() + "' of " +
                    "enumeration class '" + enumClass + "'", e);
        }
    }

    public int[] sqlTypes() {
        return sqlTypes;
    }

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

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

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

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

    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    public boolean isMutable() {
        return false;
    }

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

Last updated: 2008-12-28

email