В третьей статье этой серии я покажу, как настроить Hibernate для преобразования любых типов данных базы данных в любой тип Java и из него и, таким образом, отделить модель базы данных от вашей объектной модели.
Пользовательское отображение типов
Hibernate является очень мощным ресурсом в любом приложении, которому необходимо сохранять данные. Например, на этой неделе мне было поручено создать объектно-ориентированную модель для устаревшей базы данных. На первый взгляд это казалось достаточно простым. Затем я обнаружил большой недостаток дизайна: по историческим причинам даты были сохранены как числа в формате ГГГГММДД. Например, 11 декабря 2009 года было 20091211. Я не мог или, скорее, не изменил бы базу данных, и все же я не хотел загрязнять свою аккуратную ОО модель Integer вместо java.util.Date.
После просмотра документации Hibernate я был уверен, что это стало возможным очень простым способом.
Создание собственного сопоставителя типов
Первый шаг, который также является самым крупным, состоит в создании пользовательского типа. Этот тип не является настоящим «типом», а отображает, который знает, как преобразовать тип базы данных в тип Java и наоборот. Для этого вам нужно только создать класс, который реализует org.hibernate.usertype.UserType . Давайте посмотрим на каждый реализованный метод подробно.
Следующий метод выдает, какой класс будет возвращен в конце процесса чтения. Поскольку я хочу Date вместо Integer, я естественно возвращаю класс Date.
public Class returnedClass() {
return Date.class;
}Следующий метод возвращает, какие типы (в константах Types ) столбцы, которые будут прочитаны, имеют. Интересно отметить, что Hibernate позволяет отображать более одного столбца, таким образом, имея ту же функцию, что и аннотация JPA @Embedded. В моем случае я читаю из одного числового столбца, поэтому я должен вернуть один массив объектов, заполненный Types.INTEGER .
public int[] sqlTypes() {
return new int[] {Types.INTEGER};
}Этот метод проверяет, являются ли возвращенные экземпляры класса неизменяемыми (как любые обычные типы Java, сохраняют примитивные типы и строки) или изменяемыми (как остальные). Это очень важно, потому что, если возвращается false, поле, использующее этот пользовательский тип, не будет проверяться, чтобы увидеть, следует ли выполнять обновление или нет. Это будет, конечно, если поле будет заменено во всех случаях (изменяемые или неизменяемые). Хотя в Java API есть большая противоречивость, Date является изменчивым, поэтому метод должен возвращать true.
public boolean isMutable() {
return true;
}Я не могу догадаться, как используется следующий метод, но API утверждает:
Возвращает глубокую копию постоянного состояния, останавливаясь на объектах и в коллекциях. Нет необходимости копировать неизменяемые объекты или нулевые значения, в этом случае можно просто вернуть аргумент.
Так как мы только что сказали, что экземпляры Date являются изменяемыми, мы не можем просто вернуть объект, но вместо этого мы должны вернуть клон: это стало возможным, потому что метод Date clone () является открытым.
public Object deepCopy(Object value) throws HibernateException {
return ((Date) value).clone();
}Следующие два метода выполняют реальную работу по чтению из базы данных и обратно. Обратите внимание, как API предоставляет объект ResultSet для чтения и объект PreparedStatement для записи.
public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
Date result = null;
if (!rs.wasNull()) {
Integer integer = rs.getInt(names[0]);
if (integer != null) {
try {
result = new SimpleDateFormat("yyyyMMdd").parse(String.valueOf(integer));
} catch (ParseException e) {
throw new HibernateException(e);
}
}
}
return result;
}
public void nullSafeSet(PreparedStatement statement, Object value, int index) throws HibernateException, SQLException {
if (value == null) {
statement.setNull(index, Types.INTEGER);
} else {
Integer integer = Integer.valueOf(new SimpleDateFormat("yyyyMMdd").format((String) value));
statement.setInt(index, integer);
}
}Следующие два метода являются реализациями equals () и hasCode () с точки зрения персистентности .
public int hashCode(Object x) throws HibernateException {
return x == null ? 0 : x.hashCode();
}
public boolean equals(Object x, Object y) throws HibernateException {
if (x == null) {
return y == null;
}
return x.equals(y);
}Для equals (), поскольку Date является изменяемым, мы не могли просто проверить на равенство объектов, поскольку тот же объект мог быть изменен.
Метод replace () используется для целей слияния. Это не может быть проще.
public Object replace(Object original, Object target, Object owner) throws HibernateException { Owner o = (Owner) owner; o.setDate(target); return ((Date) original).clone(); }Моя реализация метода replace () не подлежит повторному использованию: должен быть известен как тип-владелец, так и имя метода-установщика, что затрудняет повторное использование пользовательского типа. Если бы я захотел использовать его повторно, тело метода должно было бы использовать пакет lang.reflect и угадать имена используемых методов. Таким образом, алгоритм создания многоразового пользовательского типа будет выглядеть следующим образом:
- перечислите все методы, которые являются сеттерами и примите целевой класс в качестве аргумента
- если нет подходящего метода, выведите ошибку
- если совпадает один метод, вызовите его с целевым аргументом
- если более одного метода совпадают, вызовите связанный метод получения, чтобы проверить, какой из них возвращает исходный объект
Следующие два метода используются в процессе кэширования, соответственно в сериализации и десериализации. Поскольку экземпляры Date являются сериализуемыми, их легко реализовать.
public Serializable disassemble(Object value) throws HibernateException
return (Date) ((Date) value).clone();
}
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return ((Date) cached).clone();
}Объявите тип на объекте
Как только пользовательский тип UserType реализован, вам нужно сделать его доступным для объекта.
@TypeDef(name="dateInt", typeClass = DateIntegerType.class)
public class Owner {
...
}Используйте тип
Последний шаг — аннотировать поле.
@TypeDef(name="dateInt", typeClass = DateIntegerType.class)
public class Owner {
private Date date;
@Type(type="dateInt")
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}Вы можете скачать исходники этой статьи здесь .
Чтобы идти дальше:
- Javadoc Hibernate UserType
- Другие примеры реализации UserType (для Hibernate v2): обратите внимание, что некоторые реализации просто ошибочны, но это дает хорошую отправную точку