У Hibernate есть много приятных функций, и они довольно хорошо документированы, но недавняя необходимость добавить простой пользовательский тип в существующее отображение заставила меня задуматься о том, как это сделать. Я хотел бы сделать это с аннотациями, а не путем изменения конфигурации Hibernate (такой подход является хорошо документированы). Вот как это делается.
Нужны два новых класса. Вы можете сделать это с одним (и примеры Hibernate делают это таким образом), но у них действительно разные функции, поэтому я написал их отдельно.
Первый — это класс, который вы хотите использовать для столбца. В моем случае мне нужна дата без миллисекунд, которая является тонкой оболочкой над java.util.Date. Вот мой класс:
/** * Oracle stores dates in DATE columns down to the second; Java stores them to the millisecond. * This occasionally can confuse Hibernate as to what data are stale. This class slices off * any milliseconds which might be present in its representation. */public class DateNoMs extends java.util.Date { private static final long serialVersionUID = 1L; /** @see java.util.Date() */ public DateNoMs() { super(); long t = getTime(); setTime(t - t%1000); } /** @see java.util.Date(long) */ public DateNoMs(long time) { super(time - time%1000); } /** * @param value */ public DateNoMs(Date value) { long t = value.getTime(); setTime(t - t%1000); } /** @see java.util.Date#setTime(long) */ @Override public void setTime(long time) { super.setTime(time - time%1000); }}
Прямо, верно? Теперь в моем классе у меня есть отображение поля:
@Column(name = "PAYMENT_DATE") private DateNoMs m_paymentDate;
Конечно, это не сработает — Hibernate будет затягивать отображение, потому что не знает, как сопоставить столбец JDBC DATE с DateNoMs — как и следовало ожидать. На данный момент нам нужны две вещи: во-первых, объект, который Hibernate может использовать для преобразования JDBC DATE в DateNoMs, и аннотация, указывающая на эту «фабрику». Фабричный класс создается путем реализации (в простейшем случае) org.hibernate.usertype.UserType. Документация в этом интерфейсе довольно тонкая, но в дистрибутиве Hibernate есть хорошие примеры. Вот моя реализация. Мне очень помогает тот факт, что мой класс (DateNoMs) очень близок к java.util.Date, а java.sql.Date расширяет java.util.Date.
/** * Map "things" (currently Oracle Date columns) to the DateNoMs. */public class DateNoMsType implements UserType { /** @see org.hibernate.usertype.UserType#assemble(java.io.Serializable, Object) */ public Object assemble(Serializable cached, @SuppressWarnings("unused") Object owner) { return cached; } /** @see org.hibernate.usertype.UserType#deepCopy(Object) */ public Object deepCopy(Object value) { if (value==null) return null; if (! (value instanceof java.util.Date)) throw new UnsupportedOperationException("can't convert "+value.getClass()); return new DateNoMs((java.util.Date)value); } /** @see org.hibernate.usertype.UserType#disassemble(Object) */ public Serializable disassemble(Object value) throws HibernateException { if (! (value instanceof java.util.Date)) throw new UnsupportedOperationException("can't convert "+value.getClass()); return new DateNoMs((java.util.Date)value); } /** @see org.hibernate.usertype.UserType#equals(Object, Object) */ public boolean equals(Object x, Object y) throws HibernateException { return x.equals(y); } /** @see org.hibernate.usertype.UserType#hashCode(Object) */ public int hashCode(Object value) throws HibernateException { return value.hashCode(); } /** @see org.hibernate.usertype.UserType#isMutable() */ public boolean isMutable() { return true; } /** @see org.hibernate.usertype.UserType#nullSafeGet(java.sql.ResultSet, String[], Object) */ public Object nullSafeGet(ResultSet rs, String[] names, @SuppressWarnings("unused") Object owner) throws HibernateException, SQLException { // assume that we only map to one column, so there's only one column name java.sql.Date value = rs.getDate( names[0] ); if (value==null) return null; return new DateNoMs(value.getTime()); } /** @see org.hibernate.usertype.UserType#nullSafeSet(java.sql.PreparedStatement, Object, int) */ public void nullSafeSet(PreparedStatement stmt, Object value, int index) throws HibernateException, SQLException { if (value==null) { stmt.setNull(index, Types.DATE); return; } if (! (value instanceof java.util.Date)) throw new UnsupportedOperationException("can't convert "+value.getClass()); stmt.setDate( index, new java.sql.Date( ((java.util.Date)value).getTime()) ); } /** @see org.hibernate.usertype.UserType#replace(Object, Object, Object) */ public Object replace(Object original, @SuppressWarnings("unused") Object target, @SuppressWarnings("unused") Object owner) { return original; } /** @see org.hibernate.usertype.UserType#returnedClass() */ @SuppressWarnings("unchecked") public Class returnedClass() { return DateNoMs.class; } /** @see org.hibernate.usertype.UserType#sqlTypes() */ public int[] sqlTypes() { return new int[] {Types.DATE}; }}
Ядром этого класса являются два метода, которые получают и устанавливают значения, связанные с моим новым типом: nullSafeSet и nullSafeGet. Важно отметить, что nullSafeGet поставляется со списком всех имен столбцов, сопоставленных с пользовательским типом данных в текущем запросе. В моем случае есть только один, но в сложных случаях вы можете сопоставить несколько столбцов одному объекту (примеры есть в документации по Hibernate).
Последним фрагментом головоломки является аннотация, которая сообщает Hibernate использовать новый класс «Тип» для генерации объектов вашего пользовательского типа путем добавления новой аннотации @Type в столбец:
@Type(type="com.gorillalogic.type.DateNoMsType") @Column(name = "PAYMENT_DATE") private DateNoMs m_paymentDate;
Аннотации @Type необходим полный путь к классу, который реализует интерфейс userType; это фабрика для создания целевого типа сопоставленного столбца.
Если вы собираетесь использовать новый тип во многих местах, вы можете сократить аннотацию @Type, выполнив typedef; Вы можете поместить это в package-info.java в любой пакет, который вам нравится (я помещаю мой в тот же пакет, что и класс UserType). Вот строка для типа, определенного выше:
@TypeDefs( { @TypeDef(name = "dateNoMs", typeClass = com.gorillalogic.type.DateNoMsType.class }) package com.gorillalogic.type;
Теперь аннотация моей колонки может выглядеть так:
@Type(type="dateNoMsType") @Column(name = "PAYMENT_DATE") private DateNoMs m_paymentDate;
Этого должно быть достаточно, чтобы вы начали.