Как настроить механизм dirty checking в Hibernate?

Введение

В моей предыдущей статье я описал механизм автоматической 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.

 

(Visited 434 times, 1 visits today)

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.