How to customize Hibernate dirty checking mechanism
Introduction
In my previous article I described the Hibernate automatic dirty checking mechanism. While you should always prefer it, there might be times when you want to add your own custom dirtiness detection strategy.
Custom dirty checking strategies
Hibernate offers the following customization mechanisms:
A manual dirty checking exercise
As an exercise, I’ll build a manual dirty checking mechanism to illustrate how easy you can customize the change detection strategy:
Self dirty checking entity
First, I’ll define a DirtyAware interface all manual dirty checking entities will have to implement:
public interface DirtyAware { Set<String> getDirtyProperties(); void clearDirtyProperties(); }
Next I am going to encapsulate our current dirty checking logic in a base class:
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)); } }
All manual dirty checking entities will have to extend this base class and explicitly flag the dirty properties through a call to the markDirtyProperty method.
The actual self dirty checking entity looks like this:
@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(); } }
Whenever a setter gets called, the associated property becomes dirty. For simplicity sake this simple exercise doesn’t cover the use case when we revert a property to its original value.
The dirty checking test
To test the self dirty checking mechanisms I’m going to run the following test case:
@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; } }); }
The Hibernate Interceptor solution
The Hibernate Interceptor findDirty callback allows us to control the dirty properties discovery process. This method may return:
- null, to delegate the dirty checking to Hibernate default strategy
- an int[] array, containing the modified properties indicies
Our Hibernate dirty checking interceptor looks like this:
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); } }
When passing this interceptor to our current SessionFactory configuration we get the following output:
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]}
The manual dirty checking mechanism has detected incoming changes and propagated them to the flushing event listener.
The lesser-known CustomEntityDirtinessStrategy
The CustomEntityDirtinessStrategy is a recent Hibernate API addition, allowing us to provide an application specific dirty checking mechanism. This interface can be implemented as follows:
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); } }
To register the CustomEntityDirtinessStrategy implementation we have to set the following Hibernate property:
properties.setProperty("hibernate.entity_dirtiness_strategy", EntityDirtinessStrategy.class.getName());
Running our test yields the following output:
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]}
Conclusion
Although the default field-level checking or the bytecode instrumentation alternative are sufficient for most applications, there might be times when you want to gain control over the change detection process. On a long-term project, it’s not uncommon to customize certain built-in mechanisms, to satisfy exceptional quality of service requirements. A framework adoption decision should also consider the framework extensibility and customization support.
- Code available on GitHub.
Reference: | How to customize Hibernate dirty checking mechanism from our JCG partner Vlad Mihalcea at the Vlad Mihalcea’s Blog blog. |