Введение
В моей предыдущей статье я описал механизм автоматической dirty проверки в Hibernate. Хотя мало кто и задумывается о том как это все работает и используют стратегию по дефолту, может много раз возникнуть ситуация, когда требуется добавить свою собственную стратегию обнаружения изменений в сущностях.
Кастомные стратегии dirty checking
Hibernate предлагает следующие механизмы кастомизации:
Пример кастомизации стратегии dirty checking
В качестве упражнения, я создам ручной механизм dirty checking для иллюстрации того, как легко можно кастомизировать стратегию обнаружений изменений.
Сущность, которая сама делает «грязную проверку»
Во-первых, я определю DirtyAware интерфейс, который должны имлементировать все сущности:
public interface DirtyAware { Set<String> getDirtyProperties(); void clearDirtyProperties(); }
Следующим шагом я инкапсулирую наш текущую логику dirty checking в базовый класс:
public abstract class SelfDirtyCheckingEntity implements DirtyAware { private final Map<String, String> setterToPropertyMap = new HashMap<String, String>(); @Transient private Set<String> dirtyProperties = new LinkedHashSet<String>(); public SelfDirtyCheckingEntity() { try { BeanInfo beanInfo = Introspector.getBeanInfo(getClass()); PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { Method setter = descriptor.getWriteMethod(); if (setter != null) { setterToPropertyMap.put(setter.getName(), descriptor.getName()); } } } catch (IntrospectionException e) { throw new IllegalStateException(e); } } @Override public Set<String> getDirtyProperties() { return dirtyProperties; } @Override public void clearDirtyProperties() { dirtyProperties.clear(); } protected void markDirtyProperty() { String methodName = Thread.currentThread().getStackTrace()[2].getMethodName(); dirtyProperties.add(setterToPropertyMap.get(methodName)); } }
Все сущности должны наследоваться от этого базового класса и явно отметить «загрязненные» свойства через вызов метода markDirtyProperty
.
Актуальная самодетектирующаяся сущность выглядит так:
@Entity @Table(name = "ORDER_LINE") public class OrderLine extends SelfDirtyCheckingEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private Long number; private String orderedBy; private Date orderedOn; public Long getId() { return id; } public Long getNumber() { return number; } public void setNumber(Long number) { this.number = number; markDirtyProperty(); } public String getOrderedBy() { return orderedBy; } public void setOrderedBy(String orderedBy) { this.orderedBy = orderedBy; markDirtyProperty(); } public Date getOrderedOn() { return orderedOn; } public void setOrderedOn(Date orderedOn) { this.orderedOn = orderedOn; markDirtyProperty(); } }
Всякий раз когда вызовется сеттер, ассоциированное свойство становится «грязным». Для упрощения принято было, что этот пример не покрывает случай, когда мы возвращаем свойство в сове первоначальное значение.
Тест dirty checking
Для тестирования механизма самообнаружения «загрязнений» я собираюсь запускать следующий тестовый код:
@Test public void testDirtyChecking() { doInTransaction(new TransactionCallable<Void>() { @Override public Void execute(Session session) { OrderLine orderLine = new OrderLine(); session.persist(orderLine); session.flush(); orderLine.setNumber(123L); orderLine.setOrderedBy("Vlad"); orderLine.setOrderedOn(new Date()); session.flush(); orderLine.setOrderedBy("Alex"); return null; } }); }
Применение Hibernate Interceptor
Функция обратного вызова findDirty в Hibernate Interceptor позволяет нам контролировать процесс обнаружения «грязных» свойств. Этот метод может возвращать:
- null, для делегирования дефолтной стратегии dirty checking Hibernate
- массив int[], содержащий индексы измененных свойств
Наш интерцептор «грязной» проверки выглядит так:
public class DirtyCheckingInterceptor extends EmptyInterceptor { @Override public int[] findDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { if(entity instanceof DirtyAware) { DirtyAware dirtyAware = (DirtyAware) entity; Set<String> dirtyProperties = dirtyAware.getDirtyProperties(); int[] dirtyPropertiesIndices = new int[dirtyProperties.size()]; List<String> propertyNamesList = Arrays.asList(propertyNames); int i = 0; for(String dirtyProperty : dirtyProperties) { LOGGER.info("The {} property is dirty", dirtyProperty); dirtyPropertiesIndices[i++] = propertyNamesList.indexOf(dirtyProperty); } dirtyAware.clearDirtyProperties(); return dirtyPropertiesIndices; } return super.findDirty(entity, id, currentState, previousState, propertyNames, types); } }
При передаче этого интерцептора нашей текущей SesionFactory конфигурации мы получим следующий вывод:
INFO [main]: c.v.h.m.l.f.InterceptorDirtyCheckingTest - The number property is dirty INFO [main]: c.v.h.m.l.f.InterceptorDirtyCheckingTest - The orderedBy property is dirty INFO [main]: c.v.h.m.l.f.InterceptorDirtyCheckingTest - The orderedOn property is dirty DEBUG [main]: o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects DEBUG [main]: n.t.d.l.SLF4JQueryLoggingListener - Name: Time:1 Num:1 Query:{[update ORDER_LINE set number=?, orderedBy=?, orderedOn=? where id=?][123,Anatoly,2014-08-20 07:35:05.649,1]} INFO [main]: c.v.h.m.l.f.InterceptorDirtyCheckingTest - The orderedBy property is dirty DEBUG [main]: o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects DEBUG [main]: n.t.d.l.SLF4JQueryLoggingListener - Name: Time:0 Num:1 Query:{[update ORDER_LINE set number=?, orderedBy=?, orderedOn=? where id=?][123,Alex,2014-08-20 07:35:05.649,1]}
Механизм ручной «грязной» проверки обнаружил входящие изменения и прокинул их к flishing event listener.
Менее известный CustomEntityDirtinessStrategy
CustomEntityDirtinessStrategy это одно из последний добавлений в Hibernate API, позволяющее нам обеспечить специфичный механизм dirty checking. Этот интерфейс может быть имплементирован так:
public static class EntityDirtinessStrategy implements CustomEntityDirtinessStrategy { @Override public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) { return entity instanceof DirtyAware; } @Override public boolean isDirty(Object entity, EntityPersister persister, Session session) { return !cast(entity).getDirtyProperties().isEmpty(); } @Override public void resetDirty(Object entity, EntityPersister persister, Session session) { cast(entity).clearDirtyProperties(); } @Override public void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) { final DirtyAware dirtyAware = cast(entity); dirtyCheckContext.doDirtyChecking( new AttributeChecker() { @Override public boolean isDirty(AttributeInformation attributeInformation) { String propertyName = attributeInformation.getName(); boolean dirty = dirtyAware.getDirtyProperties().contains( propertyName ); if (dirty) { LOGGER.info("The {} property is dirty", propertyName); } return dirty; } } ); } private DirtyAware cast(Object entity) { return DirtyAware.class.cast(entity); } }
Для регистрации имплементации CustomEntityDirtinessStrategy мы должны установить следующее совйство Hibernate:
properties.setProperty("hibernate.entity_dirtiness_strategy", EntityDirtinessStrategy.class.getName());
Запуск нашего теста дает следующий результат:
INFO [main]: c.v.h.m.l.f.CustomEntityDirtinessStrategyTest - The number property is dirty INFO [main]: c.v.h.m.l.f.CustomEntityDirtinessStrategyTest - The orderedBy property is dirty INFO [main]: c.v.h.m.l.f.CustomEntityDirtinessStrategyTest - The orderedOn property is dirty DEBUG [main]: o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects DEBUG [main]: n.t.d.l.SLF4JQueryLoggingListener - Name: Time:1 Num:1 Query:{[update ORDER_LINE set number=?, orderedBy=?, orderedOn=? where id=?][123,Vlad,2014-08-20 12:51:30.068,1]} INFO [main]: c.v.h.m.l.f.CustomEntityDirtinessStrategyTest - The orderedBy property is dirty DEBUG [main]: o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects DEBUG [main]: n.t.d.l.SLF4JQueryLoggingListener - Name: Time:0 Num:1 Query:{[update ORDER_LINE set number=?, orderedBy=?, orderedOn=? where id=?][123,Alex,2014-08-20 12:51:30.068,1]}
Заключение
Хотя дефолтная проверка на уровне полей или интрументация байткода эффективны для большинства приложений, может быть много ситуаций, когда Вам понадобится получать контроль над процессом обнаружения изменений. В долгосрочных проектах не такау уж и редкость, чтобы настроить некоторые встроенные механизмы, для удовлетворения исключительных требований качества сервиса.
Код будет доступен на GitHub.