Friday, June 1, 2012

How to implement XPO’s Soft Delete pattern for NHibernate

Soft Delete is a persistence pattern where instead of removing entities physically you just mark them as deleted by setting a fixed value in one of columns.

For example, in XPO (DevExpress Persistence Objects) an additional int column called GCRecord holds a null value (not deleted) or anything than null (deleted).

There’s a long discussion whether or not such approach is a pattern or an antipattern but this is beyond this post. What I would like to show is how to get soft deletes with NHibernate so that you can migrate your XPO application to NHibernate and keep your database untouched.

Filtering the data

There are two issues to solve. First – NHibernate should automatically append “filters” so that all queries will select only the data which is not deleted. I’ve blogged on this few months ago. An example code which sets filters for XPO-compatible database is:

// add this for fluent configuration
config.AddFilterDefinition( 
   new NHibernate.Engine.FilterDefinition( 
       "GCRecordFilter", null, new Dictionary<string, IType>(), true ) );
 
// create filters programatically
foreach ( var mapping in config.ClassMappings )
{
    mapping.AddFilter( "GCRecordFilter", "GCRecord is null" );
    foreach ( var property in mapping.PropertyIterator )
        if ( property.Value is Bag )
        {
            Bag bagProperty = (Bag)property.Value;
 
            bagProperty.AddFilter( "GCRecordFilter", "GCRecord is null" );
        }
}

and then you just enable filters:

_session.EnableFilter( "GCRecordFilter" );

Because of filters, NHibernate will append the “GCRecrod is null” to queries and subqueries thus simulating the way XPO filters the data.

Note that NHibernate’s feature of filtering is way, way more advanced – you are free to define any filter you need while XPO has one builtin filter which cannot be easily changed (can it be changed at all?).

Persisting the data

The remaining task is to teach NHibernate to mark records as deleted when it comes to deleting data. This is done with an event listener:

public class SoftDeleteEventListener : DefaultDeleteEventListener
{
    protected override void DeleteEntity( IEventSource session, object entity,
        EntityEntry entityEntry, bool isCascadeDeleteEnabled,
        IEntityPersister persister, ISet transientEntities )
    {
        // be smart - if the entity implements a marking interface - softdelete it
        if ( entity is ISoftDeletable )
        {
            var e = (ISoftDeletable)entity;
            e.GCRecord = 1;
 
            CascadeBeforeDelete( session, persister, entity, entityEntry, transientEntities );
            CascadeAfterDelete( session, persister, entity, transientEntities );
        }
        // else delete it
        else
        {
            base.DeleteEntity( session, entity, entityEntry, isCascadeDeleteEnabled,
                              persister, transientEntities );
        }
    }
}

which is applied by:

config.SetListener( NHibernate.Event.ListenerType.Delete, new SoftDeleteEventListener() );

Audit logging the data

Event listeners can be used not only to soft delete the data. Suppose for example that your entity holds a simple auditing info (insert date, modify date):

public interface ISoftDeletable
{
    int? GCRecord { get; set; }
    DateTime? ModifyDate { get; set; }
    DateTime? InsertDate { get; set; }
}

You can set up an event lister to automatically set values to the two columns:

public class CustomSaveEventListener : DefaultSaveEventListener
{
    protected override object PerformSaveOrUpdate( SaveOrUpdateEvent evt )
    {
        ISoftDeletable entity = evt.Entity as ISoftDeletable;
        if ( entity != null )
            ProcessEntityBeforeInsert( entity );
 
        return base.PerformSaveOrUpdate( evt );
    }
 
    internal virtual void ProcessEntityBeforeInsert( ISoftDeletable entity )
    {
        if ( entity.InsertDate == null )
            entity.InsertDate = DateTime.Now;
        entity.ModifyDate = DateTime.Now;
    }
}
 
public class CustomUpdateEventListener : DefaultUpdateEventListener
{
    protected override object PerformSaveOrUpdate( SaveOrUpdateEvent evt )
    {
        ISoftDeletable entity = evt.Entity as ISoftDeletable;
        if ( entity != null )
            ProcessEntityBeforeInsert( entity );
 
        return base.PerformSaveOrUpdate( evt );
    }
 
    internal virtual void ProcessEntityBeforeInsert( ISoftDeletable entity )
    {
        if ( entity.InsertDate == null )
            entity.InsertDate = DateTime.Now;
        entity.ModifyDate = DateTime.Now;
    }
}

and apply listeners:

config.SetListener( NHibernate.Event.ListenerType.Save, new CustomSaveEventListener() );
config.SetListener( NHibernate.Event.ListenerType.Update, new CustomUpdateEventListener() );

Putting all this toghether, it is possible to migrate your XPO persistence layer to NHibernate without losing the Soft Delete feature.

No comments: