В этой статье речь пойдет о нюансах реализации вспомогательных (но не менее важных) методов интерфейса
UserType
: deepCopy
, isMutable
, disassemble
, assemble
и replace
. О том для чего и как в целом использовать UserType
можно прочитать в первой части статьи. В этой статье для примера будут также использоваться классы User, AuditDate и AuditDateUserType из первой части.
Итак, для правильной реализации пользовательского типа первым делом нужно решить является ли Ваш объект mutable (изменяемым). Пример пользовательского типа AuditDateUserType в первой части статьи как раз является реализацией типа для изменяемого объекта. Теперь рассмотрим в чем же разница. Для immutable объектов предназначена, пожалуй, сама простая (и самая распространенная) реализация перечисленных выше методов. Так как поля объекта никогда не будут изменяться и для определения изменения состояния достаточно выполнить проверку равенства по ссылке:
@Override public boolean equals(Object x, Object y) throws HibernateException { if (null == x || null == y) { return false; } return x == y; } @Override public int hashCode(Object x) throws HibernateException { if (x != null) { return x.hashCode(); } return 0; } @Override public Object deepCopy(Object value) throws HibernateException { return value; } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; }
deepCopy
— используется для создания снапшотов состояния объекта после любой персистент операции. Среди перечисленных здесь методов является, пожалуй, самым важным, потому что именно от качества снапшотов объекта и реализации методаequals
зависит дальнейшая обработка текущего состояния объекта. Название метода говорит само за себя, он должен возвращать полную копию объекта. Но это справедливо только для mutable объектов. Для immutable же напротив с целью оптимизации метод должен возвращать не копию, а просто принимаемый аргумент. А в случае если equals объектов осуществляется по ссылке, то не возвращать копию объекта просто жизненно важно, иначе вы получите непредвиденные апдейты во время закрытия транзакции и других операций.isMutable
— возвращает true, если объект типа может изменяться. Используется для оптимизации при работе с объектами, содержащими поля immutable типа, внутри коллекций.disassemble
— конвертирует объект в вид, пригодный для хранения в кэше второго уровня.
assemble
— восстанавливает объект из сериализованного вида при получении объекта из кэша второго уровня.
В случае с изменяемыми объектами эти два метода должны возвращать полную копию объектов.replace
— вызывается когда необходимо выполнить слияние двух объектов. Для immutable достаточно вернуть первый аргумент, для mutable объекта следует вернуть полную копию первого аргумента. Лишь в некоторых случаях для составных объектов есть смысл копировать значения полей по отдельности.
Для того, чтобы разобраться как это работает и на что влияет, вернемся к классам в первой части статьи, убедимся, что все работает и потом … все сломаем.
Сперва проверим как работает существующая реализация AuditDateUserType, выполнив следующий код:
@Test public void auditDataSaveTest() { Session session = HibernateUtil.getSessionFactory().openSession(); session.beginTransaction(); // создадим в БД нового пользователя User user = new User(); user.setName("John"); user.setAudit(new AuditDate(new LocalDate(), new LocalDate())); session.save(user); session.refresh(user); System.out.println(user.getAudit().getModifiedDate()); // изменим дату его редактирования на 1 день user.getAudit().setModifiedDate(new LocalDate().plusDays(1)); session.flush(); session.refresh(user); System.out.println(user.getAudit().getModifiedDate()); 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_.m 2013-10-17 Hibernate: update user set createdDate=?, modifiedDate=?, name=? where id=? Hibernate: select user0_.id as id0_0_, user0_.createdDate as createdD2_0_0_, user0_.m 2013-10-18
Вывод в консоль подтверждает, что пользовательский тип работает так, как и ожидалось: запись была создана и после изменения корректно обновлена.
Теперь изменим реализацию AuditDateUserType так, чтобы он считал объект AuditDate неизменяемым, несмотря на то, что AuditDate на самом деле остается mutable.
AuditDateUserType:
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 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()); } } // имплементация методов для immutable объектов @Override public boolean equals(Object x, Object y) throws HibernateException { if (null == x || null == y) { return false; } // больше не делегируем вызов метода equals AuditDate return x == y; } @Override public int hashCode(Object x) throws HibernateException { if (x != null) { return x.hashCode(); } return 0; } @Override public Object deepCopy(Object value) throws HibernateException { return value; } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable) value; } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; }
Теперь, если мы попробуем выполнить тот же auditDataSaveTest
, то получим следующую картину:
Hibernate: insert into user (createdDate, modifiedDate, name) values (?, ?, ?) Hibernate: select user0_.id as id0_0_, user0_.createdDate as createdD2_0_0_, user0_.m 2013-10-17 Hibernate: select user0_.id as id0_0_, user0_.createdDate as createdD2_0_0_, user0_.m 2013-10-17
Мы видим, что обновление записи не происходит. Почему? Hibernate не видит наших изменений потому что сам объект AuditDate остался тот же, а изменения его полей мы больше не отслеживаем.
При такой реализации мы предполагаем, что поля объекта не могут изменяться, а только ссылка на сам объект. Если в auditDataSaveTest
заменить строку
user.getAudit().setModifiedDate(new LocalDate().plusDays(1));
на инициализацию нового объекта AuditDate
user.setAudit(new AuditDate(new LocalDate(), new LocalDate().plusDays(1)));
тогда мы все-таки получим ожидаемое нами обновление.
Все методы описанные в этой статье работают взаимосвязано, в случае возникновения ошибок нельзя исправить работу только одного из них. Например, выполнение методов deepCopy
и equals
всегда логически связано, нельзя возвращать в deepCopy
копию объекта без корректной проверки равенства снапшотов в equals
.