Tuesday, April 27, 2010

Soft Deletes in Nhibernate

The following was an email to my dev colleagues, sorry about the code formatting Im using the Blogger web interface (OSX really needs a WLW equivlient!):


Fellow nerds;
There are frequently requirements that we need to have soft deletes. These things are a pain, especially with an ORM as they can start becoming invasive to your domain code.

If you want soft deletes to be transparent as possibly in normal workings of the domain you can use a DeleteListener to intercept the deletion and, well, soften it.
Below is some code to show a parent and child relationship (ParentSampleDomainObject and SampleDomainObject).

Key points:
• The entities need to have a flag to show they are deleted eg IsDeleted
• The mapping on the parent needs to say that it is not interested in retrieving deleted items by using a where clause
• The mapping also need to say it want to delete orphan children, (we won’t actually delete them, but this needs to be here)
• We need to have an NHibernate delete listener to intercept the cascading delete and instead of deleting the entity mark it as deleted. This is why we need cascading deletes in the mapping
• We need to register the listener to the config.

The test code will be checked into the core soon.

Rhys

Nhibernate Configuration:
<?xml version="1.0" encoding="UTF-8"?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory name="applicationregister">
        <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
        <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
        <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
        <property name="proxyfactory.factory_class">NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle</property>
        <property name="connection.connection_string">Data Source=.\SqlExpress;Database=CoreTests;Integrated Security=True;</property>
        <property name="connection.isolation">ReadCommitted</property>
        <property name="default_schema">CoreTests.dbo</property>
        <mapping assembly="RhysC.NHibernate.Tests"/>
        <event type="save-update">
            <listener class="RhysC.NHibernate.AuditListener,RhysC.NHibernate"/>
        </event>
        <event type="save">
            <listener class="RhysC.NHibernate.AuditListener,RhysC.NHibernate"/>
        </event>
        <event type="delete">
            <listener class="RhysC.NHibernate.SoftDeleteListener,RhysC.NHibernate"/>
        </event>
    </session-factory>
</hibernate-configuration>

Parent Domain entities mapping file:
<?xml version="1.0" encoding="UTF-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="NHibernate.Tests" assembly="NHibernate.Tests">
    <class name="ParentSampleDomainObject" table="ParentSampleDomainObject">
        <id name="Id" column="ParentSampleDomainObjectId">
            <generator class="identity"/>
        </id>
        <bag name="Children" inverse="true" lazy="true" cascade="all-delete-orphan" where="IsDeleted = 0">
            <key column="ParentSampleDomainObjectId" foreign-key="ParentSampleDomainObjectId_FK"/>
            <one-to-many class="SampleDomainObject"/>
        </bag>
    </class>
</hibernate-mapping>

[Serializable]
public class ParentSampleDomainObject : BaseEntity<int>
{
public ParentSampleDomainObject()
{
Children = new List<SampleDomainObject>();
}
public virtual IList<SampleDomainObject> Children { get; set; }
public virtual void AddChild(SampleDomainObject child)
{
Children.Add(child);
child.Parent = this;
}
public virtual void RemoveChild(SampleDomainObject child)
{
Children.Remove(child);
}
}
[Serializable]
public class SampleDomainObject : BaseEntity<int> , ISoftDeletable
{
public virtual string Name { get; set; }
public virtual bool IsDeleted { get; set; }
public virtual void MarkAsDeleted()
{
IsDeleted = true;
}
public virtual ParentSampleDomainObject Parent { get; protected internal set; }
}

public class SoftDeleteListener : DefaultDeleteEventListener
{
protected override void DeleteEntity(IEventSource session, object entity,
EntityEntry entityEntry, bool isCascadeDeleteEnabled,
IEntityPersister persister, ISet transientEntities)
{
if (entity is ISoftDeletable)
{
var e = (ISoftDeletable)entity;
e.MarkAsDeleted();
if(entity is IAuditableRecord)
{
var a = (IAuditableRecord) entity;
AuditListener.SetAuditInfo(a);//need to have a log of when this was actually deleted, probably the intent of the soft delete!
}
CascadeBeforeDelete(session, persister, entity, entityEntry, transientEntities);
CascadeAfterDelete(session, persister, entity, transientEntities);
}
else
{
base.DeleteEntity(session, entity, entityEntry, isCascadeDeleteEnabled,
persister, transientEntities);
}
}
}

No comments: