Для начала приведу более подробную постановку задачи. Допустим, у нас есть поле, которое принимает одно из несколько значений. Эти значения нам заранее известны. Наиболее удачное решение - это объявить такое поле как enum. Например, так:
enum State { Draft, Active } public class Document { State state; ... }
Наша задача наиболее оптимальным способом сохранить поле state сущности Document в базе при помощи библиотеки hibernate. Для того, чтобы hibernate мог сохранять объекты, нужно указать ему как это нужно сделать. Есть два способа:
- при помощи xml-файла с описанием - xml-мэппингом,
- с помощью аннотаций.
Сейчас мы рассмотрим первый способ. Итак, нам нужно описать в xml-файле мэппинг перечисляемого типа State.
Hibernate из коробки предлагает нам два варианта:
- Сохранять название (Draft, Active)
- Сохранять порядковый номер (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
Возможное решение данной проблемы - указать свой собственный мэппинг для каждого перечисляемого поля. Для этого нужно:
- Определить отдельный класс наследуемый от UserType
- В мэппинге указать ссылку на него.
Например, как описано в этом блоге
Но такое решение требует написание класса для каждого перечисляемого типа. Хочется универсального варианта, который бы требовал минимального количество кода. То есть, при добавлении 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