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]} 

If you enjoyed this article, I bet you are going to love my book as well.

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 the exceptional quality of service requirements. A framework adoption decision should also consider the framework extensibility and customization support.

Code available on GitHub.

If you liked this article, you might want to subscribe to my newsletter too.

Advertisements

2 thoughts on “How to customize Hibernate dirty checking mechanism

  1. Seems like a lot of effort compared to using an ORM that supports automatic dirty checking right ‘out of the box’ like DataNucleus or some of the other ORMs that use byte code enhancement for this purpose. Even Hibernate has implemented byte code enhancement for this but I am yet to see anyone using Hibernate in that mode – apparently it had some reliability issues early on.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s