Статьи

Как настроить механизм проверки грязи в Hibernate

Вступление

В моей предыдущей статье я описал механизм автоматической проверки грязи Hibernate. Несмотря на то, что вы всегда должны предпочитать это, могут быть ситуации, когда вы захотите добавить свою собственную стратегию обнаружения загрязнения.

Пользовательские грязные стратегии проверки

Hibernate предлагает следующие механизмы настройки:

Ручная грязная проверка

В качестве упражнения я создам ручной механизм грязной проверки, чтобы показать, насколько легко вы можете настроить стратегию обнаружения изменений:

Сам грязная проверка сущности

Во-первых, я определю интерфейс DirtyAware, который должны быть реализованы всеми объектами, выполняющими ручную проверку:

1
2
3
4
5
6
public interface DirtyAware {
 
    Set<String> getDirtyProperties();
 
    void clearDirtyProperties();
}

Далее я собираюсь инкапсулировать нашу текущую грязную логику проверки в базовый класс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 .

Фактически самозагрязненный объект проверки выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@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();
    }
}

Всякий раз, когда вызывается сеттер, связанное свойство становится грязным. Для простоты это простое упражнение не охватывает случай использования, когда мы возвращаем свойство к его первоначальному значению.

Грязный проверочный тест

Чтобы проверить механизмы самозагрязненной проверки, я собираюсь запустить следующий тестовый пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@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

Обратный вызов Hibernate Interceptor findDirty позволяет нам контролировать процесс обнаружения грязных свойств. Этот метод может вернуть:

  • null , чтобы делегировать грязную проверку стратегии по умолчанию Hibernate
  • массив int [], содержащий измененные свойства

Наш перехватчик Hibernate выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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);
        }
    }

При передаче этого перехватчика в нашу текущую конфигурацию SessionFactory мы получаем следующий вывод:

1
2
3
4
5
6
7
8
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,Vlad,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]}

Механизм ручной проверки грязи обнаружил входящие изменения и распространил их на слушатель события очистки.

Менее известный CustomEntityDirtinessStrategy

CustomEntityDirtinessStrategy — это недавнее дополнение API Hibernate, позволяющее нам обеспечить механизм грязной проверки для конкретного приложения. Этот интерфейс может быть реализован следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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:

1
properties.setProperty("hibernate.entity_dirtiness_strategy", EntityDirtinessStrategy.class.getName());

Выполнение нашего теста дает следующий вывод:

1
2
3
4
5
6
7
8
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 .