Skip to main content

Hibernate And Mapping enum to customized values

With Hibernate, enums can be easily mapped either by enum item name or the position of each item but what if you want to map it to a customized value? In my case, we have so many one-character long columns in our tables, representing flags, statuses etc. We have heaps of them. Writing UserTypes for each enum field is very boring and nasty job. As every where you see in internet, you need to keep a Map for each user-type in order to map those values to enum elements.

So to avoid this, I ended up with something more clean, easy and more generic.

Now imagine you have following enum:


public enum PaymentFrequencyEnum {

       WEEKLY("WK"),
       FORTNIGHTLY("FN"),
       MONTHLY("MT"),
       QUARTERLY("QL"),
       YEARLY("YL");
       
       private String value;    
  
       private PaymentFrequency(String value) {
            this.value = value;
       } 
}




I've chosen two-letter code as value so that you understand my point of writing this post :)
BUT you may have different values, for example you may want to map WEEKLY to an expression representing the calculation of a payment, say WEEKLY("$X * 7") or MONTHLY("$X * 30").

Anyway... Now to do this, firstly we need to define an interface, let's call it Enumable:



package com.test.hibernate.usertype;


import java.io.Serializable;

/**
 * To be used with Hibernate user type.  
 * @author Mohammad Norouzi
 */
public interface Enumable extends Serializable {

 /**
  * Returns the internal value of each enum to be persisted in hibernate.
  * @return The internal string value.
  */
 public String getValue();

    /**
     * Returns the enum item from the given value.
     * @param value value.
     * @return enum.
     */
    public Enum getEnumFromValue(String value);

    /**
     * A helper class to find corespondent enum by a value.
     */
    class EnumableHelper {
        /**
         * Method to find corespondent enum by a provide value, if value can't match will throw an exception.
         * This will be used when database field has restricted values and not allow undefined values (constraint).
         * @param e The enum class instance
         * @param value The value to be matched
         * @return Enum which matched the value.
         * @throw IllegalArgumentException
         *          Thrown when the value can not be match to a enum.
         */
        public static Enum getEnumFromValue(Enum e, String value) {
            Assert.notNull(e, "Enum object cannot be null");

            Enum aE = getEnumFromValue(e, value, null);

            if (aE != null) {
                return aE;
            } else {
                throw new IllegalStateException("Invalid value [" + value + "] for enum class [" + e.getClass() + "]");
            }
        }

        /**
         * Method to find corespondent enum by a provide value, if value can't match will throw an exception.
         * This will be used when database field has restricted values and not allow undefined values (constraint).
         * @param e The enum class instance
         * @param value The value to be matched
         * @param defaultEnum The default Enum will be returned if null detected.
         * @return Enum which matched the value, otherwise return the defaultEnum provided
         */
        public static Enum getEnumFromValue(Enum e, String value, Enum defaultEnum) {
            Assert.notNull(e, "Enum object cannot be null");
            Enum[] enums = e.getClass().getEnumConstants();

            for (Enum aE : enums) {
                if (!Enumable.class.isAssignableFrom(aE.getClass())) {
                    throw new IllegalArgumentException("Enum Must implement Enumable!");
                }
                final Enumable ge =  (Enumable) aE;

                if ( ("" + ge.getValue()).equals(("" + value)) ) {
                    return aE;
                }
            }

            return defaultEnum;
        }
    }
}



Next thing is to extend all your enum from this interface. The inner class defined in interface is just a helper class you'll see its use in following code:

public enum PaymentFrequencyEnum extends Enumable {

       WEEKLY("WK"),
       FORTNIGHTLY("FN"),
       MONTHLY("MT"),
       QUARTERLY("QL"),
       YEARLY("YL");
       
       private String value;    
  
       private PaymentFrequencyEnum(String value) {
            this.value = value;
       }
       public String getValue() {
          return this.value;
       } 

       public Enum getEnumFromValue(String value) {
          return EnumableHelper.getEnumFromValue(this, value, null);
       }
}



Unfortunately we can't use Abstract classes for enum so we have to implement those two methods for each enum, now I hope you understand why I have created the Helper class.

Next thing is to create a general Hibernate user type:



package com.test.hibernate.usertype;

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.usertype.EnhancedUserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * Hibernate User type for enums.
 * @author Mohammad Norouzi
 * @see Enumable
 * @param  any enum type that implements Enumable
 *
 */
public class GeneralEnumMapUserType<T extends Enumable> implements EnhancedUserType, Serializable {


    private static final long serialVersionUID = -5993020929647717601L;
    private Class enumClass;
    private Enumable FIRST_ENUM_ITEM;

    /**
     * Constructor.
     * @param enumClass enum class
     *
     */
    protected GeneralEnumMapUserType(Class%lt;T> enumClass) {
        this.enumClass = enumClass;
        if(enumClass.getEnumConstants()[0] instanceof Enumable) {
            FIRST_ENUM_ITEM = enumClass.getEnumConstants()[0];
        } else {
            throw new IllegalStateException("The class " + enumClass + " MUST implement Enumable interface!");
        }
    }

    @Override
    public int[] sqlTypes() {
        return new int[]{StandardBasicTypes.STRING.sqlType()};
    }

    @Override
    public Class returnedClass() {
        return Enum.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if(x != null) {
            return x.equals(y);
        }
        return false;
    }

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

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        String name = rs.getString(names[0]);
        return rs.wasNull() || name == null ? null : FIRST_ENUM_ITEM.getEnumFromValue(name);
    }

    @Override
    public void nullSafeSet(PreparedStatement st,
                            Object value, int index, SessionImplementor session) throws SQLException {
        if (value == null) {
            st.setNull(index, StandardBasicTypes.STRING.sqlType());
        } else {
            st.setString(index, ((Enumable)value).getValue());
        }
    }

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

    @Override
    public boolean isMutable() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        // TODO Auto-generated method stub
        return (Serializable) value;
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        // TODO Auto-generated method stub
        return cached;
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        // TODO Auto-generated method stub
        return original;
    }

    @Override
    public String objectToSQLString(Object value) {
        return '\'' + ((Enumable)value).getValue() + '\'';
    }

    @Override
    public String toXMLString(Object value) {
        return ((Enumable) value).getValue();
    }

    @Override
    public Object fromXMLString(String xmlValue) {
        return xmlValue == null || xmlValue.isEmpty() ? null : FIRST_ENUM_ITEM.getEnumFromValue(xmlValue);
    }
}



Almost finished. At this point we only need a user type for our enum which is very simple:


package com.test.hibernate.usertype;

import com.test.PaymentFrequencyEnum;
import com.test.hibernate.usertype.GeneralEnumMapUserType;

public class PaymentFrequencyEnumUserType extends GeneralEnumMapUserType<PaymentFrequencyEnum> {
    /**
     * Constructor.
     *
     */
    public PaymentFrequencyEnumUserType() {
        super(PaymentFrequencyEnum.class);
    }
}



All done! You just need to introduce your user type to Hibernate. If you have another enum, all you need is above simple class!

Comments

Anonymous said…
I have been looking for something like this for 2 days. Worked great for me, thanks
Anonymous said…
Nice, thanks! I modified this to a slightly different version which does not need a specific UserType (eg PaymentFrequencyEnumUserType).