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

Popular posts from this blog

CI/CD Automation: How to wait for Helm deployment into Kubernetes cluster

 So I created this post because with Helm v3 , if you use --wait option and the timeout is reached and the deployment isn't successful, Helm marks the deployment as failed . The problem is that subsequent deployment (upgrade) that may contain the fix won't work as expected and it will end up in error like this: [some-name] has no deployed release   The only way to fix this is manual intervene and deleting the previous failed release. This is not only scary but also against the automation process. If we remove --wait option, Helm will mark the deployment as successful regardless. My solution to this that works nicely is as per below: Remove --wait option from helm deploy Use this command to retrieve the list of deployment for that namespace that you are deploying against: kubectl get deployments -n ${namespace} -o jsonpath='{range .items[*].metadata}{.name}{","}{end}' You can use split to turn the comma separated list above into an array Then you can run mul...

JSF or GWT?

I have worked with JSF for almost 4 years. First time when I had a chance to work with GWT, I was very excited. Like many Java developers I was like 'Wow I don't need to play with those bloody tags and elements and now it's pure Java code!!!'... I was so happy but my happiness didn't last very long. Programming is my passion. I hate writing codes that become a mess later and unfortunately this is what will eventually happen with GWT. The thing is you can't rely on reviews and features of a software or an API, in action everything goes different way. Specially when it comes to large scaled application and in an environement where everything is urgent and importnat... well, I think all software companies are the same in this regard... The fact is that in a team of developers not every one cares about best practices, design patterns and even if you have very experienced designer, solution architect etc, still you can't force the team to deve...