Мэппинг перечисляемых типов (Enum) в Hibernate

Для начала приведу более подробную постановку задачи. Допустим, у нас есть поле, которое принимает одно из несколько значений. Эти значения нам заранее известны. Наиболее удачное решение - это объявить такое поле как enum. Например, так:

enum State { Draft, Active }

public class Document
{
   State state;

   ...
}

Наша задача наиболее оптимальным способом сохранить поле state сущности Document в базе при помощи библиотеки hibernate. Для того, чтобы hibernate мог сохранять объекты, нужно указать ему как это нужно сделать. Есть два способа:

  1. при помощи xml-файла с описанием - xml-мэппингом,
  2. с помощью аннотаций.

Сейчас мы рассмотрим первый способ. Итак, нам нужно описать в xml-файле мэппинг перечисляемого типа State.

Hibernate из коробки предлагает нам два варианта:

  1. Сохранять название (Draft, Active)
  2. Сохранять порядковый номер (0, 1), который возвращает метод ordinal()

Оба эти варианта описываются при помощи следующего мэппинга:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="ru.zvv.example.Document">
   <id name="id">
      <generator class="native"/>
   </id>

   <property name="state">
      <type name="org.hibernate.type.EnumType">
         <param name="enumClass">ru.zvv.example.State</param>
      </type>
   </property>
</class>
</hibernate-mapping>

Способ определяется в зависимости от типа колонки в таблице БД.

Плюсы очевидны - мы имеем достаточно простое объявление перечисляемого поля и прозрачное преобразование Season в поле базы данных.

Какие же минусы у этих вариантов мэппинга? Запись названия enum-константы в строковое поле чревато проблемами: во-первых, в дальнейшем нельзя будет изменить название, а во-вторых, строковые сравнения медленнее числовых.

Другой способ, запись порядкового номера, накладывает не очевидное ограничение: при добавлении новой константы в середину списка enum, мы получим сдвиг порядковых номеров, и как следствие искажение данных.

Но эти способы также не позволяют сохранять enum-поля, числовые значения которых не совпадают с ordinal(). Например, если для статуса draft при проектирование БД зарезервировали числовое значение 1, а для active - 10

Возможное решение данной проблемы - указать свой собственный мэппинг для каждого перечисляемого поля. Для этого нужно:

  1. Определить отдельный класс наследуемый от UserType
  2. В мэппинге указать ссылку на него.

Например, как описано в этом блоге

Но такое решение требует написание класса для каждого перечисляемого типа. Хочется универсального варианта, который бы требовал минимального количество кода. То есть, при добавлении enum-свойства:

  • не хочется определять какие-либо вспомогательные классы для каждого нового enum
  • в БД enum должен отображаться в виде целого числа, причем это число не должно является порядковым номером enum-константы.

И такое решение есть. Для этого достаточно написать замену EnumType, который бы использовал пользовательский метод для получения числового значения, записываемого в БД. Этот метод логично разместить в отдельном интерфейсе:

public interface IEnumType
{
   int getDbValue();
}

И далее создать универсальный преобразователь работающий с интерфейсом:

public class EnumHibernateType implements UserType, ParameterizedType, 
                                          Serializable
{
   ...
  
   public Object nullSafeGet(final ResultSet resultSet,
                             final String[] names,
                             final Object owner) throws SQLException
   {
      int value = resultSet.getInt(names[0]);
      return resultSet.wasNull() ? null : valueOf(value);
   }

   public void nullSafeSet(final PreparedStatement statement,
                           final Object value,
                           final int index) throws SQLException
   {
      if (value == null)
         statement.setNull(index, Types.INTEGER);
      else
         statement.setInt(index, ((IEnumType) value).getDbValue());
   }

   private Object valueOf(final int value)
   {
      for (Object o : returnedClass().getEnumConstants())
      {
         IEnumType type = (IEnumType) o;
         if (type.getDbValue() == value)
            return type;
      }
      return null;
   }

   ...
}

Полностью исодный код данного класса можно взять c github

Теперь мы можем использовать преобразователь следующим образом:

<property name="state">
   <type name="ru.zvv.hibernate.EnumHibernateType">
      <param name="enumClass">ru.zvv.example.State</param>
   </type>
</property>

Осталось только имплементировать интерфейс IEnumType в нашем enum'е:

public enum State implements IEnumType
{
   Draft(1), Active(10);

   private final int dbValue;
   
   State(final int dbValue)
   {
      this.dbValue = dbValue;
   }

   @Override
   public int getDbValue()
   {
       return dbValue;
   }

}

Исходные коды данной статьи можно взять с github