При работе с Hibernate потребность в пользовательских типах возникает довольно часто. Несмотря на то, что базовые типы позволяют решать достаточно большой спектр задач, рано или поздно возникает ситуация когда просто необходимо изменить стандартное поведение при преобразовании определенного типа данных или класса. Чаще всего пользовательские типы используются в следующий случаях:
- сохранение типа Java в колонке с SQL типом, отличным от того, который использует Hibernate;
- разделение одного значения свойства и сохранение результата в более чем одном столбце базы данных;
- сохранение нескольких свойств в одном столбце;
- использование кастомного класса в качестве идентификатора для сущности.
На практике же этот список намного длиннее, практически ни один частный случай не обходится без использования пользовательских типов. Основной смысл их создания заключается в том, чтобы указать Hibernate каким образом он должен преобразовывать определенные типы в их представление в БД.
Для того, чтобы создать пользовательский тип необходимо реализовать один из интерфейсов, предоставляемых Hibernate. Основным и наиболее распространенным из них является org.hibernate.usertype.UserType
.
UserType
Этот интерфейс предоставляет основные методы для определения пользовательского типа (hibernate-core-4.1.9.Final):
public interface UserType { public int[] sqlTypes(); public Class returnedClass(); public boolean equals(Object x, Object y) throws HibernateException; public int hashCode(Object x) throws HibernateException; public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException; public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException; public Object deepCopy(Object value) throws HibernateException; public boolean isMutable(); public Serializable disassemble(Object value) throws HibernateException; public Object assemble(Serializable cached, Object owner) throws HibernateException; public Object replace(Object original, Object target, Object owner) throws HibernateException; }
Некоторые из этих методов, такие как sqlTypes
, returnedClass
, nullSafeGet
и nullSafeSet
, пожалуй являются наиболее понятными и очевидными. Но все же поговорим немного об их реализации:
sqlTypes()
— должен возвращать массив кодов SQL-типов для генерации DDL схемы, каждый код для отдельной колонки в БД. Для получения кода можно использовать напрямую sql типы (Types.VARCHAR
), или Hibernate типы, что в свою очередь позволит Hibernate выбирать актуальный sql тип в зависимости от диалекта базы данных (StandardBasicTypes.STRING.sqlType()
)returnedClass()
— метод сообщает Hibernate какой Java класс отображает этот пользовательский тип.nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
— метод, который извлекает значение свойства из JDBC Resultset. Количество и порядок колонок в массивеnames
точно соответствуют набору, возвращаемому методомsqlTypes
.owner
— текущий получаемый из базы экземпляр сущности.nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
— пишет значения свойства в PreparedStatement. Если в типе несколько столбцов, то они записываются в параметры начиная со значенияindex
.
По правде говоря, перечисленные выше методы, пожалуй, единственные, которые хотелось бы реализовывать при создании пользовательского типа. Именно так и поступает большинство программистов при создании своего первого пользовательского типа, используя готовые реализации остальных методов, найденные в интернете, и особо не задумываясь об их правильной имплементации. Но когда тип перестает работать так как ожидалось, неизбежно приходится разбираться в том, как же все эти методы должны работать. О правильной реализации deepCopy
, isMutable
, assemble
и прочих методов речь пойдет во второй части этой статьи.
Небольшой пример имплементации и использования пользовательского типа для нескольких столбцов
AuditDate — класс, который мы будем использовать как кастомный тип данных в сущности User для хранения информации о дате создания и изменения объекта. Допустим, что в нашем случае изменение этой информации должно происходить программно на уровне репозитория:
import java.io.Serializable; import org.joda.time.LocalDate; public class AuditDate implements Serializable { private LocalDate createdDate; private LocalDate modifiedDate; public AuditDate() { } public AuditDate(LocalDate createdDate, LocalDate modifiedDate) { this.createdDate = createdDate; this.modifiedDate = modifiedDate; } public LocalDate getCreatedDate() { return createdDate; } public void setCreatedDate(LocalDate createdDate) { this.createdDate = createdDate; } public LocalDate getModifiedDate() { return modifiedDate; } public void setModifiedDate(LocalDate modifiedDate) { this.modifiedDate = modifiedDate; } @Override public int hashCode() { int hash = 7; hash = 31 * hash + (null == createdDate ? 0 : createdDate.hashCode()); hash = 31 * hash + (null == modifiedDate ? 0 : modifiedDate.hashCode()); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof AuditDate)) { return false; } AuditDate ad = (AuditDate) obj; return (this.createdDate == ad.createdDate || (this.createdDate != null && this.createdDate.equals(ad.createdDate))) && (this.modifiedDate == ad.modifiedDate || (this.modifiedDate != null && this.modifiedDate.equals(ad.modifiedDate))); } }
Он просто содержит дополнительные поля и не реализует никаких специфических методов или интерфейсов Hibernate. Подразумеваем, что это обычные поля в таблице user
базы данных, которые мы хотим поместить в отдельный объект. В классе User нам нужно указать Hibernate как именно мы хотим обрабатывать свойство audit
, для этого используется аннотация Type
, в которой указывается реализация конкретного пользовательского типа. Для пользовательских типов с несколькими колонками в БД необходимо также явно указать Columns
, иначе получите MappingException: «property mapping has wrong number of columns».
@Entity @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @Type(type = "org.project.hibernate.AuditDateUserType") @Columns(columns = { @Column(name = "createdDate"), @Column(name = "modifiedDate") }) private AuditDate audit; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public AuditDate getAudit() { return audit; } public void setAudit(AuditDate audit) { this.audit = audit; } }
И, собственно, имплементация пользовательского типа, который отвечает за загрузку и сохранение AuditDate в соответствующие поля БД:
public class AuditDateUserType implements UserType { @Override public int[] sqlTypes() { return new int[] { StandardBasicTypes.TIMESTAMP.sqlType(), StandardBasicTypes.TIMESTAMP.sqlType() }; } @Override public Class<AuditDate> returnedClass() { return AuditDate.class; } @Override public boolean equals(Object x, Object y) throws HibernateException { if (null == x || null == y) { return false; } return x.equals(y); } @Override public int hashCode(Object x) throws HibernateException { return x.hashCode(); } @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { AuditDate auditData = new AuditDate(); Timestamp created = rs.getTimestamp(names[0]); if (created != null) { auditData.setCreatedDate(new LocalDate(created)); } Timestamp modified = rs.getTimestamp(names[1]); if (created != null) { auditData.setModifiedDate(new LocalDate(modified)); } return auditData; } @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { AuditDate auditData = (AuditDate) value; if (null != auditData.getCreatedDate()) { Timestamp createdTimestamp = new Timestamp(auditData .getCreatedDate().toDateTimeAtStartOfDay().getMillis()); st.setTimestamp(index, createdTimestamp); } else { st.setNull(index, StandardBasicTypes.TIMESTAMP.sqlType()); } if (null != auditData.getModifiedDate()) { Timestamp modifiedTimestamp = new Timestamp(auditData .getModifiedDate().toDateTimeAtStartOfDay().getMillis()); st.setTimestamp(index + 1, modifiedTimestamp); } else { st.setNull(index + 1, StandardBasicTypes.TIMESTAMP.sqlType()); } } @Override public Object deepCopy(Object value) throws HibernateException { AuditDate auditData = (AuditDate) value; AuditDate copy = new AuditDate(auditData.getCreatedDate(), auditData.getModifiedDate()); return copy; } @Override public boolean isMutable() { return true; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) this.deepCopy(value); } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return this.deepCopy(cached); } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return this.deepCopy(original); }
Обратите внимание, что в данном случае реализация методов equals
и hashCode
полностью зависит от правильной имплементации аналогичных методов внутри объекта AuditDate.
Убедимся, что Hibernate правильно воспринимает наш тип данных:
Session session = HibernateUtil.getSessionFactory().openSession(); session.beginTransaction(); User user = new User(); user.setName("John"); AuditDate auditDate = new AuditDate(new LocalDate(), new LocalDate()); user.setAudit(auditDate); session.save(user); session.refresh(user); System.out.println(user.getAudit().getCreatedDate()); session.getTransaction().commit(); session.close();
Лог:
Hibernate: insert into user (createdDate, modifiedDate, name) values (?, ?, ?) Hibernate: select user0_.id as id0_0_, user0_.createdDate as createdD2_0_0_, user0_.modifiedDate as modified3_0_0_, user0_.name as name0_0_ from user user0_ where user0_.id=? 2013-07-05
В заключение, хотелось бы отметить своего рода недостаток использования UserType интерфейса, о котором нужно задуматься еще на стадии проектирования. UserType интерфейс не выставляет индивидуальные свойства, такие как createdDate, в метамодель Hibernate. Поэтому Вы не сможете писать HQL запросы или Criteria с указанием свойств UserType. Для получения такой функциональности можно использовать другой интерфейс CompositeUserType
.