Member Menu
 
 Monthly JBoss newsletter:
 
Hibernate Books
CaveatEmptor

Complex Validations using Interceptor

Hibernate offers a couple of different ways to implement validation checking. Typically, entities are validated either internally by implementing the Validatable interface or externally by implementing the Interceptor interface. Either is a viable approach for simple "invariant" type checking (not null checks, range checks, etc).

However, both of these approaches have the (sometimes severe) limitation of not allowing session manipulation during "lifecycle" callbacks. For the Validatable interface, this includes all of its callback methods; for Interceptor interface, this includes the onFlushDirty(), onSave(), and onDelete() methods. What this means is that a validation done in these methods cannot perform a query (nor can they even trigger a lazy initialization!).

One solution is to simply perform these validations in the application's business tier prior to persisting the entities. However, this tends to lead to code duplication, or even worse forgetting to code the validation check in a certain execution branch.

The other solution, which will be discussed here, is to defer validations till after the flush using the Interceptor.onPostFlush() callback. The onPostFlush() method does not have the limitation of not being allowed to perform calls on the session. This approach does have the drawback that validations do not occur until after SQL statements have been executed against the RDBMS, but even this can be a "pro" as it allows all validation messages across all entities involved in a flush to be collected at a single time.

Below is a sample implemenation of such an interceptor. It assumes a base interface or class for a mapped entities named IDomainEntity:

/* ValidationInterceptor.java */
package example.community.complexvalidator;

import net.sf.hibernate.Interceptor;

import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Hashtable;

public class ValidationInterceptor implements Interceptor, Serializable
{
    private List inserts = new ArrayList();
    private List deletes = new ArrayList();
    private Map updates = new Hashtable();

    public boolean onLoad(
            Object entity, Serializable id,
            Object[] state,
            String[] propertyNames, Type[] types)
    {
        return false;
    }

    public boolean onSave(
            Object entity, Serializable id,
            Object[] state,
            String[] propertyNames, Type[] types)
    {
        inserts.add(entity);
        return false;
    }

    public boolean onFlushDirty(
            Object entity, Serializable id,
            Object[] currentState, Object[] previousState,
            String[] propertyNames, Type[] types)
    {
        DeltaSet changes = DeltaSetCalculator.calculateDeltaSet(propertyNames,
            previousState, currentState);
        updates.put(entity, changes);
        return false;
    }

    public void onDelete(
            Object entity, Serializable id,
            Object[] state,
            String[] propertyNames, Type[] types)
    {
        deletes.add(entity);
    }

    public void preFlush(Iterator entities)
    {
        // Can add invariant checking here...
    }

    public void postFlush(Iterator entities)
    {
        // This implementation does not attempt to "bunch" validations for the entire
        // flush.  Instead, as soon as an entity fails, the exception is thrown.
        try
        {
            for (Iterator insertItr = inserts.iterator(); insertItr.hasNext(); )
            {
                Validator.validateCreation(insertItr.next());
            }

            for (Iterator updateItr = updates.entrySet().iterator();
                 updateItr.hasNext(); )
            {
                final Map.Entry updateEntry = (Map.Entry)updateItr.next();
                final Object entity = updateEntry.getKey();
                final DeltaSet changes = (DeltaSet)updateEntry.getValue();
                Validator.validateModification(entity, changes);
            }

            for (Iterator deleteItr = deletes.iterator(); deleteItr.hasNext(); )
            {
                Validator.validateDeletion(deleteItr.next());
            }
        }
        catch (ValidationException e)
        {
            throw new ValidationExceptionRuntimeWrapper(e);
        }
        finally
        {
            inserts.clear();
            updates.clear();
            deletes.clear();
        }
    }
    ...
}

Just be careful that "calculating changes" does not cause initialization of proxies or lazy collections. Below is a set of utility classes I use to "determine changes".

A general interface to represent a change:

/* PropertyDelta.java */
package example.community.complexvalidator;

import java.io.Serializable;

public interface PropertyDelta extends Serializable
{
    public static class Type {}
    public static final Type SIMPLE = new Type();
    public static final Type COLLECTION = new Type();
    public static final Type ASSOCIATION = new Type();

    public String getPropertyName();
    public Class getPropertyType();
    public Type getDeltaType();
}

And its various implementations:

/* SimplePropertyDelta.java */
package example.community.complexvalidator;

import example.community.complexvalidator.PropertyDelta;

public class SimplePropertyDelta implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Object oldValue;
    private Object newValue;

    public SimplePropertyDelta(
        String propertyName, Class propertyType,
        Object oldValue, Object newValue)
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Object getOldValue()
    {
        return oldValue;
    }

    public Object getNewValue()
    {
        return newValue;
    }

    public PropertyDelta.Type getDeltaType()
    {
        return PropertyDelta.SIMPLE;
    }

}
/* CollectionPropertyDelta.java */
package example.community.complexvalidator;

import example.community.complexvalidator.PropertyDelta;

import java.util.Arrays;
import java.util.Collection;
import java.util.Set;

public class CollectionPropertyDelta implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Set additions = new HashSet();
    private Set removals = new HashSet();

    public CollectionPropertyDelta(
        String propertyName, Class propertyType,
        Collection oldValue, Collection newValue)
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        calculateAdditionsAndRemovals(oldValue, newValue);
    }

    public CollectionPropertyDelta(
        String propertyName, Class propertyType,
        Object[] oldValue, Object[] newValue)
    {
        this(propertyName, propertyType,
            Arrays.asList(oldValue), Arrays.asList(newValue));
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Set getAdditions()
    {
        return Collections.unmodifiableSet(additions);
    }

    public Set getRemovals()
    {
        return Collections.unmodifiableSet(removals);
    }

    public boolean anyChangeDetected()
    {
        return !getAdditions().isEmpty() && !getRemovals().isEmpty();
    }

    public PropertyDelta.Type getDeltaType()
    {
        return PropertyDelta.COLLECTION;
    }

    private void calculateAdditionsAndRemovals(
        Collection oldValue, Collection newValue)
    {
        // First, determine additions
        if (oldValue != null)
        {
            additions.removeAll(oldValue);
        }
        if (newValue != null)
        {
            additions.addAll(newValue);
        }
        // Then, determine removals
        if (newValue != null)
        {
            removals.removeAll(newValue);
        }
        if (oldValue != null)
        {
            removals.addAll(oldValue);
        }
    }
}
/* AssociationPropertyDelta.java */
package example.community.complexvalidator;

import example.community.complexvalidator.PropertyDelta;

public class AssociationPropertyDelta implements PropertyDelta
{
    private String propertyName;
    private Class propertyType;
    private Object oldId;
    private Object newId;

    public AssociationPropertyDelta(
        String propertyName, Class propertyType,
        Long oldId, Long newId)
    {
        this.propertyName = propertyName;
        this.propertyType = propertyType;
        this.oldId = oldId;
        this.newId = newId;
    }

    public String getPropertyName()
    {
        return propertyName;
    }

    public Class getPropertyType()
    {
        return propertyType;
    }

    public Object getOldValue()
    {
        return oldId;
    }

    public Object getNewValue()
    {
        return newId;
    }

    public Type getDeltaType()
    {
        return PropertyDelta.ASSOCIATION;
    }
}

Then a container to hold a collection of changes:

/* DeltaSet.java */
package example.community.complexvalidator;

import java.io.Serializable;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;

public class DeltaSet implements Serializable
{
    private Set deltas = new java.util.HashSet();
    private Map deltaPropertyNames = new HashMap();

    DeltaSet() {}

    void addDelta(PropertyDelta delta)
    {
        deltas.add(delta);
        deltaPropertyNames.put(delta.getPropertyName(), null);
    }

    public Set getDeltas()
    {
        return java.util.Collections.unmodifiableSet(deltas);
    }

    public boolean wasDelta(String propertyName)
    {
        return deltaPropertyNames.containsKey(propertyName);
    }

    void clear()
    {
        deltas.clear();
    }

}

Finally, a convenience class to parse all the changes from a given domain entity (taking great care to not trigger lazy initializations):

/* DeltaSetCalculator.java */
package example.community.complexvalidator;

import example.community.complexvalidator.CollectionPropertyDelta;
import example.community.complexvalidator.DeltaSet;
import example.community.complexvalidator.IDomainEntity;

import java.text.SimpleDateFormat;
import java.util.logging.Logger;
import java.util.Collection;

public class DeltaSetCalculator
{
    private static final Logger log = Logger.getLogger(DeltaSetCalculator.class);
    private static final SimpleDateFormat sdf =
        new SimpleDateFormat("YYYY-MM-dd HH:mm:ss:SSS");

    private DeltaSetCalculator()
    {
    }

    /**
     * A hibernate-specific calculation.  Uses the values passed to the Hibernate
     * <code>Interceptor.onFlushDirty()</code> to perform the calculation.
     *
     * @param propertyNames A string array of all the property names passed in to
     *                      the obFlushDirty method.
     * @param previousState The Object array representing the previous state of
     *                      the properties named in the propertyNames array.
     * @param currentState The Object array representing the current state of
     *                     the properties named in the propertyNames array.
     * @return The DeltaSet representing the changes encountered in the property
     *         states.
     */
    public static DeltaSet calculateDeltaSet(
        String[] propertyNames,
        Object[] previousState, Object[] currentState)
    {
        if (propertyNames == null || previousState == null || currentState == null)
        {
            throw new IllegalArgumentException(
                "All three arrays passed to calculate a delta-set must be non-null" );
        }
        if (propertyNames.length != previousState.length
            && previousState.length != currentState.length)
        {
            throw new IllegalArgumentException(
                "All three arrays passed to calculate a delta-set must be of the"
                + " same length");
        }

        DeltaSet deltaSet = new DeltaSet();
        try
        {
            for (int i = 0; i < propertyNames.length; i++)
            {
                log.debug("Starting property [" + propertyNames[i] + "]");
                final Object propertyPreviousState = previousState[i];
                final Object propertyCurrentState = currentState[i];
                final boolean wasPreviousNull = propertyPreviousState == null;
                final boolean isCurrentNull = propertyCurrentState == null;

                // Try to determine the property type from either currentState or,
                // previousState...  Side-note: if both are null, we cannot determine
                // the propertyType, but thats OK as no change has occurred (null==null)
                final Class propertyType;
                if (wasPreviousNull && isCurrentNull)
                {
                    log.debug("Both were null; skipping");
                    continue;
                }
                else if (!isCurrentNull)
                {
                    if (propertyCurrentState instanceof IDomainEntity)
                    {
                        propertyType = IDomainEntity.class;
                    }
                    else
                    {
                        propertyType = propertyCurrentState.getClass();
                    }
                }
                else // !wasPreviousNull
                {
                    if (propertyPreviousState instanceof IDomainEntity)
                    {
                        propertyType = IDomainEntity.class;
                    }
                    else
                    {
                        propertyType = propertyPreviousState.getClass();
                    }
                }

                if (Hibernate.isInitialized(propertyPreviousState)
                    || Hibernate.isInitialized(propertyCurrentState))
                {
                    final PropertyDelta delta = getDeltaOrNull(
                            propertyNames[i], propertyType,
                            propertyPreviousState, propertyCurrentState);
                    if (delta != null)
                    {
                        deltaSet.addDelta(delta);
                    }
                }
            }
        }
        catch (Throwable t)
        {
            log.error("Error determining delta-set", t);
        }
        finally
        {
            log.debug("Done delta-set determination");
        }
        return deltaSet;
    }


    /**
     * General use DeltaSet calculator.
     */
    public static DeltaSet calculateDeltaSet(Object obj1, Object obj2)
    {
        if (obj1 == null || obj2 == null)
        {
            throw new IllegalArgumentException(
                "Both objects passed to calculate a delta-set must be non-null");
        }

        DeltaSet deltaSet = new DeltaSet();
        try
        {
            BeanInfo beanInfo =
                Introspector.getBeanInfo(obj1.getClass(), Object.class);
            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();

            for (int i = 0; i < pds.length; i++)
            {
                final String propertyName = pds[i].getName();
                final Class propertyType = pds[i].getPropertyType();
                final Object oldValue = PropertyUtils.getProperty(obj1, propertyName);
                final Object newValue = PropertyUtils.getProperty(obj2, propertyName);

                final PropertyDelta delta =
                    getDeltaOrNull(propertyName, propertyType, oldValue, newValue);
                if (delta != null)
                {
                    deltaSet.addDelta(delta);
                }
            }
        }
        catch (Throwable t)
        {
            log.error("Error determining delta-set", t);
        }
        finally
        {
            log.debug("Done delta-set determination");
        }
        return deltaSet;
    }

    public static PropertyDelta getDeltaOrNull(
        String propertyName, Class propertyType,
        Object oldValue, Object newValue)
    {
        PropertyDelta delta = null;
        log.debug("Checking property [name=" + propertyName
            + ", type=" + propertyType + "]");

        if (IDomainEntity.class.isAssignableFrom(propertyType))
        {
            log.debug("Encountered property is an association type");

            final Long oldId = (oldValue == null) ? null
                : ((IDomainEntity) oldValue).getId();
            final Long newId = (newValue == null) ? null
                : ((IDomainEntity) newValue).getId();
            if (!areEqual(oldId, newId))
            {
                delta = new AssociationPropertyDelta(
                    propertyName, propertyType,
                    oldId, newId);
            }
        }
        else if (Collection.class.isAssignableFrom(propertyType))
        {
            log.debug("Encountered property is a collection type");

            Collection oldCollectionValue = (Collection) oldValue;
            Collection newCollectionValue = (Collection) newValue;

            if (Hibernate.isInitialized(oldCollectionValue)
                && Hibernate.isInitialized(newCollectionValue))
            {
                CollectionPropertyDelta collectionDelta = new CollectionPropertyDelta(
                    propertyName, propertyType,
                    oldCollectionValue, newCollectionValue
                );
                if (collectionDelta != null && collectionDelta.anyChangeDetected())
                {
                    delta = collectionDelta;
                }
                collectionDelta = null;
            }
            else
            {
                log.info("One (or both) of a collection property was not previously"
                    + " initialized; have to skip" );
            }
        }
        else if (propertyType.isArray())
        {
            log.debug("Encountered property is an array type");

            CollectionPropertyDelta collectionDelta = new CollectionPropertyDelta(
                propertyName, propertyType,
                (Object[]) oldValue, (Object[]) newValue);
            if (collectionDelta != null && collectionDelta.anyChangeDetected())
            {
                delta = collectionDelta;
            }
            collectionDelta = null;
        }
        else
        {
            log.debug("Property was a simple property");
            if (!areEqual(oldValue, newValue))
            {
                delta = new SimplePropertyDelta(
                    propertyName, propertyType,
                    oldValue, newValue);
            }
        }

        if (delta == null)
        {
            log.debug("No delta occurred");
        }
        else
        {
            log.debug("Delta encountered");
        }
        return delta;
    }

    public static boolean areEqual(Object obj1, Object obj2)
    {
        if (obj1 == null && obj2 == null)
        {
            log.debug("Both were null");
            return true;
        }
        else if (obj1 == null || obj2 == null)
        {
            log.debug("One or the other were null (but not both)");
            return false;
        }
        else if (Date.class.isAssignableFrom(obj1.getClass())
            || Timestamp.class.isAssignableFrom(obj1.getClass())
            || java.sql.Date.class.isAssignableFrom(obj1.getClass())
            || Time.class.isAssignableFrom(obj1.getClass()))
        {
            Date d1 = (Date) obj1;
            Date d2 = (Date) obj2;
            return d1.equals(d2) || d2.equals(d1);
        }
        else
        {
            log.debug("Checking [" + obj1 + "] against [" + obj2 + "]");
            return obj1.equals(obj2);
        }
    }

}

Using Session.isDirty() to Perform Complex Validations

Here's a variation of the above, using the new method Session.isDirty().

It addresses the issue in the comment below about database constraints.

Background: You want to use Hibernate to enforce invariants on your persistent entities. Every time Hibernate updates an object in the database, you want a special validate method to be called. You can do this by implementing the Validatable interface, but Hibernate doesn't allow you to use the session, trigger lazy loads, etc., in Validatable.validate(). That's fairly limiting.

You can instead use an interceptor that collects the objects that have been updated, and then after flush is finished, validate all of them. This is the approach outlined in the 'Complex Validations using Interceptor' Wiki topic. We independently came up with the same approach about a year ago for our project. (Although we didn't mess with the '=ChangeDeltaSet=' calculation stuff. If any property of the entity has changed, we validate the whole entity. We do pass the list of dirty properties by diffing the previous/currentState arrays Hibernate passes to onFlushDirty().)

This works pretty well, but validation occurs after the update. If there is a 'non-null' constraint on the database, it's too late, we've already blown up with an SQLException. The integrity of your domain model is still preserved, but it's hard to report a meaningful error to the user.

Another solution mentioned in the Wiki topic is to use Validatable, but start a new session in validate(), and validate the entity with that session. The problem there is that the temporary sesssion won't share the same IdentityMap, etc., so you could end up loading the same entity twice, having two diferent instances of the same logical entity in memory, one with any applied changes and one without, etc. So that's out.

We can improve things by using the Session.isDirty() method. This method acts just like flush(), except it doesn't really issue any SQL statements to the database, and Interceptor.postFlush() is not called. Interceptor.onFlushDirty(), however, is called. So, we can write an interceptor that captures the updated entities (like before), and write our own wrapper method for Session.flush() which validates the dirty entities before flushing. The Interceptor would look like this:

/* CollectingInterceptor.java */
package example.community.complexvalidator;

import net.sf.hibernate.Interceptor;

import java.io.Serializable;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;

public class CollectingInterceptor implements Interceptor
{
    private static class IdentityKey
    {
        private Serializable id;
        private Class clazz;

        public IdentityKey(Serializable id, Class clazz)
        {
            this.id = id;
            this.clazz = clazz;
        }

        public boolean equals(Object obj)
        {
            IdentityKey rhs = (IdentityKey) obj;
            return id.equals(rhs.id) && clazz.equals(rhs.clazz);
        }

        public int hashCode()
        {
            return id.hashCode() * 37 + clazz.hashCode();
        }
    }
  
    private Map dirtyEntitiesMap = new HashMap();

    public boolean onFlushDirty(
        Object entity, Serializable id,
        Object[] currentState, Object[] previousState,
        String[] propertyNames, Type[] types)
        throws CallbackException
    {
        dirtyEntitiesMap.put(new IdentityKey(id, entity.getClass()), entity);
    }

    public Set getDirtyEntities()
    {
        return dirtyEntitiesMap.values();
    }

    // other methods just return their default values...
}

You would pass an instance of this Interceptor to SessionFactory.buildSession().

The wrapper method around Session.flush() might look like this:

/* MySessionWrapper.java */
package example.community.complexvalidator;

import example.community.complexvalidator.CollectingInterceptor;
import example.community.complexvalidator.MyValidatable; /* see below... */

import net.sf.hibernate.Session;

import java.util.Iterator;
import java.util.Set;

public class MySessionWrapper
{
    private Session session;
    private CollectingInterceptor interceptor;

    public MySessionWrapper(Session session, CollectingInterceptor interceptor)
    {
        this.session = session;
        this.interceptor = interceptor;
    }

    public Set getDirtyEntities()
    {
        session.isDirty();
        return interceptor.getDirtyEntities();
    }

    public void flush()
    {
        for (Iterator iter = getDirtyEntities().iterator(); iter.hasNext(); )
        {
            validate(iter.next());
        }
    }

    private void validate(Object entity)
    {
        if (obj instanceof MyValidatable) {
            MyValidatable validatable = (MyValidatable) iter.next();
            validatable.validate();
        }
    }
}

You could of course implement onSave() to collect inserted entities and validate them too. Any entity that wants automatically invoked validation would implement the MyValidatable interface, which would look like this:

/* MyValidatable.java */
package example.community.complexvalidator;

public interface MyValidatable {
    public void validate();
}

If you add a validateForDelete() method to this interface, and collect deleted entities in CollectingInterceptor.onDelete(), you could validate before deleted entities as well.

It might be nice if Hibernate had a '=Collection Session.getDirtyEntities()=' method. Then we wouldn't need a custom Interceptor.

Steve Molitor

smolitor@erac.com


  NEW COMMENT

Very bad formatting on this message. 04 Nov 2003, 04:52 amezick
Be nice if I could view it in 1024x768 without scrolling sideways.
 
what about database constraints? 22 Nov 2003, 02:50 jonjs
Thanks for this contribution.

Consider, though, the problem where you have a uniqueness constraint on 
a colummn in your database.  When this is violated, the database will 
throw an exception as  part of the flush. Because of the exception, your 
postFlush interceptor code will not get called.

The good news is that the transaction fails and the database is 
protected.  The bad news is that it is pretty much impossible to convert 
the exception into a meaningful message for the end user.  In fact the 
exact exception that is thrown depends on the appServer and/or database 
being used.  It will not provide enough information to let you form a 
reasonable message like "a User is already defined with this user-id, 
please choose another".

From the point of view of providing a nice UI, it would be better to use 
the database as a last line of defense and catch the error in java code.  
But this means doing a Query, which has all the problems you mention.

One solution is to use the Validateable interface but solve the session 
limitation problem by opening a new session that exists only for the 
validate() call.  Any o
 
Possible "Race" condition in db-query for uniqueness 16 Mar 2004, 22:45 eepstein
Of course if you do use a separate query to guarantee uniqueness then 
you need a transaction that is spanning the queries.  Otherwise in a 
pseudo-race condition this will not ensure uniqueness.  E.g.,

a) check name "foo" is unique --> result = true
b) insert record with name "foo"
a) insert record with name "foo" --> fails!
 
re: db constraints 16 Apr 2004, 02:02 steve
There is actually (what I think is) a very elegant and simple solution 
to the issue you bring up.  What we do to to explicitly define our 
constraints (we never use the schema generation stuff), which gives 
you the ability to name the constraint.  Then, when an exception does 
occur we (using the Spring framework) convert the jdbc exception to 
spring's DataAccessException.  If the exception happens to be 
something like a constraint violation, we then parse the name of the 
contraint violated and use the name in a resource bundle lookup for 
display to the end user.  Can't get much more user friendly than that; 
and I don't have to jump through any more hoops than necessary to do 
this vaidation thing.
 
Event system. 09 Oct 2006, 12:22 cesnek
What about events system in hibernate. Has event methods same 
limitation of not allowing session manipulation ?
 
bad testing logic 07 Jun 2008, 11:13 jeitemgie
if (propertyNames.length != previousState.length
            && previousState.length != currentState.length)

(1 != 2) && (2 != 3) -> true  ok
(1 != 1) && (1 != 2) -> false !!! WRONG should be true
(1 != 2) && (2 != 2) -> false !!! WRONG should be true
(1 != 1) && (1 != 1) -> false ok


should be

if (propertyNames.length != previousState.length
            || previousState.length != currentState.length)

(1 != 2) || (2 != 3) -> true  ok
(1 != 1) || (1 != 2) -> true ok
(1 != 2) || (2 != 2) -> true ok
(1 != 1) || (1 != 1) -> false ok
 
© Copyright 2006, Red Hat Middleware, LLC. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc. [Privacy Policy]