Saturday, October 3, 2009

User Type Enumeration in Hibernate Annotations

There are times we would like to work with enum types in Hibernate, but we would like the enum name to be different then the enum value in the database. For example, we would like the Java enum name to be in upper case and the database enum name to be in lower case.

This feature can be achieved by creating a Hibernate user type enumeration. The code for this kind of enumeration can be found on the web, but I will post this code here anyway (that it will be easier). The main goal of this post, is to demonstrate how such a user type enumeration can be used with Annotations.

Here are the classes for creating the user type enumerations:

First is the interface defining a method: “getValue”, which returns the actual value of the enumeration. This value is the value that is written to the database table:

package com.bashan.blog.persistence;
/**
 * Utility class designed to allow dinamic fidding and manipulation of Enum
 * instances which hold a string value.
 */
public interface StringValuedEnum {
    /**
     * Current string value stored in the enum.
     * @return string value.
     */
    public String getValue();
}

The second class:

package com.bashan.blog.persistence;
/**
 * Utility class designed to inspect StringValuedEnums.
 */
public class StringValuedEnumReflect
{
  /**
   * Don't let anyone instantiate this class.
   *
   * @throws UnsupportedOperationException Always.
   */
  private StringValuedEnumReflect()
  {
    throw new UnsupportedOperationException("This class must not be instanciated.");
  }
  /**
   * All Enum constants (instances) declared in the specified class.
   *
   * @param enumClass Class to reflect
   * @return Array of all declared EnumConstants (instances).
   */
  private static <T extends Enum> T[]
  getValues(Class<T> enumClass)
  {
    return enumClass.getEnumConstants();
  }
  /**
   * All possible string values of the string valued enum.
   *
   * @param enumClass Class to reflect.
   * @return Available string values.
   */
  public static <T extends Enum & StringValuedEnum> String[]
  getStringValues(Class<T> enumClass)
  {
    T[] values = getValues(enumClass);
    String[] result = new String[values.length];
    for (int i = 0; i < values.length; i++)
    {
      result[i] = values[i].getValue();
    }
    return result;
  }
  /**
   * Name of the enum instance which hold the especified string value.
   * If value has duplicate enum instances than returns the first occurency.
   *
   * @param enumClass Class to inspect.
   * @param value     String.
   * @return name of the enum instance.
   */
  public static <T extends Enum & StringValuedEnum> String
  getNameFromValue(Class<T> enumClass, String value)
  {
    T[] values = getValues(enumClass);
    for (int i = 0; i < values.length; i++)
    {
      if (values[i].getValue().compareTo(value) == 0)
      {
        return values[i].name();
      }
    }
    return "";
  }
}

And the last one:

package com.bashan.blog.persistence;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;
import java.lang.reflect.*;
import org.hibernate.HibernateException;
import org.hibernate.usertype.EnhancedUserType;
import org.hibernate.usertype.ParameterizedType;
import static com.todacell.ui.model.persistence.StringValuedEnumReflect.*;
//Please notice the calls to getNameFromValue *************************
public class StringValuedEnumType<T extends Enum & StringValuedEnum>
    implements EnhancedUserType, ParameterizedType
{
  /**
   * Enum class for this particular user type.
   */
  private Class<T> enumClass;
  /**
   * Value to use if null.
   */
  private String defaultValue;
  /**
   * Creates a new instance of ActiveStateEnumType
   */
  public StringValuedEnumType()
  {
  }
  public void setParameterValues(Properties parameters)
  {
    String enumClassName = parameters.getProperty("enum");
    try
    {
      enumClass = (Class<T>)Class.forName(enumClassName).asSubclass(Enum.class).
          asSubclass(StringValuedEnum.class); //Validates the class but does not eliminate the cast
    } catch (ClassNotFoundException cnfe)
    {
      throw new HibernateException("Enum class not found", cnfe);
    }
    setDefaultValue(parameters.getProperty("defaultValue"));
  }
  public String getDefaultValue()
  {
    return defaultValue;
  }
  public void setDefaultValue(String defaultValue)
  {
    this.defaultValue = defaultValue;
  }
  /**
   * The class returned by <tt>nullSafeGet()</tt>.
   *
   * @return Class
   */
  public Class returnedClass()
  {
    return enumClass;
  }
  public int[] sqlTypes()
  {
    return new int[]{Types.VARCHAR};
  }
  public boolean isMutable()
  {
    return false;
  }
  /**
   * Retrieve an instance of the mapped class from a JDBC resultset. Implementors
   * should handle possibility of null values.
   *
   * @param rs    a JDBC result set
   * @param names the column names
   * @param owner the containing entity
   * @return Object
   * @throws HibernateException
   * @throws SQLException
   */
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
            throws HibernateException, SQLException {
        String value = rs.getString( names[0] );
        if (value==null) {
            value = getDefaultValue();
            if (value==null){ //no default value
                return null;
            }
        }
        String name = getNameFromValue(enumClass, value);
        Object res = name == null ? null : Enum.valueOf(enumClass, name);
        return res;
    }

  /**
   * Write an instance of the mapped class to a prepared statement. Implementors
   * should handle possibility of null values. A multi-column type should be written
   * to parameters starting from <tt>index</tt>.
   *
   * @param st    a JDBC prepared statement
   * @param value the object to write
   * @param index statement parameter index
   * @throws HibernateException
   * @throws SQLException
   */
  public void nullSafeSet(PreparedStatement st, Object value, int index)
      throws HibernateException, SQLException
  {
    if (value == null)
    {
      st.setNull(index, Types.VARCHAR);
    }
    else
    {
      st.setString(index, ((T)value).getValue());
    }
  }
  public Object assemble(Serializable cached, Object owner)
      throws HibernateException
  {
    return cached;
  }
  public Serializable disassemble(Object value) throws HibernateException
  {
    return (Enum)value;
  }
  public Object deepCopy(Object value) throws HibernateException
  {
    return value;
  }
  public boolean equals(Object x, Object y) throws HibernateException
  {
    return x == y;
  }
  public int hashCode(Object x) throws HibernateException
  {
    return x.hashCode();
  }
  public Object replace(Object original, Object target, Object owner)
      throws HibernateException
  {
    return original;
  }
  public String objectToSQLString(Object value)
  {
    return '\'' + ((T)value).getValue() + '\'';
  }
  public String toXMLString(Object value)
  {
    return ((T)value).getValue();
  }
  public Object fromXMLString(String xmlValue)
  {
    String name = getNameFromValue(enumClass, xmlValue);
    return Enum.valueOf(enumClass, name);
  }
}

Here is an example how this user type enumeration is used:

package com.bashan.blog.persistence;
public enum Gender implements StringValuedEnum {
  MALE("male"),
  FEMALE("female");
  private final String gender;
  Gender(final String gender)
  {
    this.gender = gender;
  }
  public String getValue()
  {
    return this.gender;
  }
}

And here is an example how the enum Gender is used as a member in an Hibernate Entity class Person:

package com.bashan.blog.persistence;
import org.hibernate.annotations.Entity;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;
import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Entity
@Table(name = "person")
public class Person implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "person_id")
    private Integer personId;
    @Column(name = "first_name")
    @NotNull
    @Length(max = 50)
    private String firstName;
    @Column(name = "last_name")
    @NotNull
    @Length(max = 50)
    private String lastName;
    @Column(name = "birth_date")
    private Date birthDate;
    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person")
    @JoinColumn(name = "person_id")
    @Column(name = "gender")
    @Type(type = "com.todacell.ui.model.persistence.StringValuedEnumType",
            parameters = @Parameter(name = "enum", value = "com.bashan.blog.persistence.Gender"))
    @NotNull
    private Gender gender;
    public Integer getPersonId() {
        return personId;
    }
    public void setPersonId(Integer personId) {
        this.personId = personId;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public Date getBirthDate() {
        return birthDate;
    }
    public void setBirthDate(Date birthDate) {
        this.birthDate = birthDate;
    }
    public Gender getGender() {
        return gender;
    }
    public void setGender(Gender gender) {
        this.gender = gender;
    }
}

The part in which the enum is used is marked in yellow.

You can grab all the sources for this post here.

2 comments:

  1. Hi Miguel,

    2 things:
    1) This code is quite old, so if you use a new version of Hibernate, thing might have changed.
    2) Can you please be more specific about what is not working. I might figure it out and help you solve it.

    ReplyDelete