Back

UserEnupTypeMapping

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

Gender uses int as database values. The methods valueOf() and name() are called from the EnumUserType class:

package org.electrickery.jscbook.master;

public enum Gender {
	FEMALE(0), MALE(1), 
	NEUTERED_MALE(2), NEUTERED_FEMALE(3), 
	ABNORMAL(4), UNKNOWN(5),
	CONTRACEPTED_MALE(6), CONTRACEPTED_FEMALE(7);
	
	private int genderCode;
	
	private Gender(int code) {
		genderCode = code;
	}
	
	// used by EnumUserType via Master.hbm.xml
	public int valueOf() {
		return genderCode;
	}
	
	public static Gender name(int code) {
		for (Gender gender : values()) {
			if (code == gender.valueOf()) { return gender; }
		}
		return null;
	}
}

BirthType uses characters as database values. I use Strings here, but chars work too. The disadvantage of char is that the character value is stored, making it the same as an int. The selection of strings as Enum placeholders is for compatibility with the original database. I might abandon this if I find a good reason:

package org.electrickery.jscbook.master;

public enum BirthType {
	CAPTIVITY("1"), WILD("2"), UNKNOWN("X");
	
	private String birthTypeCode;
	
	private BirthType(String bType) {
		birthTypeCode = bType;
	}
	
	// used by EnumUserType via Master.hbm.xml
	public String valueOf() {
		return birthTypeCode;
	}
	
	public static BirthType name(String code) {
		for (BirthType birthType : values()) {
			if (birthType.valueOf().equals(code)) { return birthType; }
		}
		return null;
	}

}

The <class> tag contains the table mapping, the <typedef>. Quite weird is that if you insert the valueOf and name method wrong into identifierMethod and valueOfMethod (I did), it still works, but the enum values are stored in the table as string. Maybe I implemented them reversed...


<!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">valueOf</param>
		<param name="valueOfMethod">name</param>
	</typedef>
	<typedef name="dateEst" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.enummap.DateEst</param>
		<param name="identifierMethod">valueOf</param>
		<param name="valueOfMethod">name</param>
	</typedef>
	<typedef name="gender" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.master.Gender</param>
		<param name="identifierMethod">valueOf</param>
		<param name="valueOfMethod">name</param>
	</typedef>
	<typedef name="rearing" class="org.electrickery.jscbook.enummap.EnumUserType">
		<param name="enumClassName">org.electrickery.jscbook.master.Rearing</param>
		<param name="identifierMethod">valueOf</param>
		<param name="valueOfMethod">name</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 = "name";
    private static final String DEFAULT_VALUE_OF_METHOD_NAME = "valueOf";

    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-11-19

email