Logo

NHibernate

The object-relational mapper for .NET

Soft Deletes

Blog Signature Gabriel

What can I do if instead of physically delete a record in the database I just want to mark it as deleted?

There are at least two possibilities to achieve the desired result

  • put the necessary logic into the repository
  • Write and register a DeleteEvent-Listener for NHibernate

The Domain Model

Let's assume a simple order entry system with just two entities Order and OrderLine.

Note: the implementation of the IdentityFieldProvider class I have discussed here.

The business requirements are such as that you are not allowed to physically delete an order from the system but just mark it as deleted in case where the user cancels an order. That's the reason why we have a property IsDeleted in the two entities. When querying for orders the system will (automatically) filter out orders having IsDeleted=true.

Let's have a look at the implementation of the Order and OrderLine entities. (Note: I have only implemented the absolute minimum needed for this sample to work. A realistic order entity would be more complex.)

public class Order : IdentityFieldProvider<Order>
{
    public virtual string CustomerName { get; set; }
    public virtual DateTime OrderDate { get; set; }
    public virtual bool IsDeleted { get; set; }
    public virtual ISet<OrderLine> OrderLines { get; set; }
 
    public Order()
    {
        OrderLines = new HashedSet<OrderLine>();
    }
}
 
public class OrderLine : IdentityFieldProvider<OrderLine>
{
    public virtual string ProductName { get; set; }
    public virtual int Amount { get; set; }
    public virtual bool IsDeleted { get; set; }
}

And here are the mappings (For this sample I have the mapping of both entities in a single XML file although the recommendations are one mapping per entity)

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="Domain"
                   namespace="Domain"
                   schema="Playground">
 
  <class name="Order" table="Orders" where="IsDeleted=0">
    <id name="Id">
      <generator class="guid"/>
    </id>
    <property name="CustomerName" not-null="true" length="50"/>
    <property name="OrderDate" not-null="true"/>
    <property name="IsDeleted" not-null="true"/>
    <set name="OrderLines" cascade="all-delete-orphan">
      <key column="OrderId"/>
      <one-to-many class="OrderLine"/>
    </set>
  </class>
 
  <class name="OrderLine" where="IsDeleted=0">
    <id name="Id">
      <generator class="guid"/>
    </id>
    <property name="ProductName" not-null="true" length="50"/>
    <property name="Amount" not-null="true"/>
    <property name="IsDeleted" not-null="true"/>
  </class>
 
</hibernate-mapping>

Note the where attribute on the <class> tag of the order mapping. This contains a filter to avoid that NHibernate returns orders marked as deleted when you query for orders.

Logic in the Repository

This is easy. Assume that we have a OrderRepository class with a Remove method which physically deletes the order from the system and a SoftDelete method witch only logically removes the order from the system.

Normal Delete Operation

The implementation of the Remove method would probably look similar to this

public void Remove(Order order)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        using (ITransaction tx = session.BeginTransaction())
        {
            session.Delete(order);
            tx.Commit();
        }
    }
}

and the SQL generated by NHibernate is as follows

NHibernate: UPDATE Playground.OrderLine SET OrderId = null WHERE OrderId = @p0; @p0 = '34e04bfd-18d2-4451-8673-b98466c6260f'
NHibernate: DELETE FROM Playground.OrderLine WHERE Id = @p0; @p0 = '82b6ba92-9eca-494e-a71f-10a2fa0012db'
NHibernate: DELETE FROM Playground.OrderLine WHERE Id = @p0; @p0 = 'bec15bd1-1216-4623-bfd9-1319fbef764b'
NHibernate: DELETE FROM Playground.Orders WHERE Id = @p0; @p0 = '34e04bfd-18d2-4451-8673-b98466c6260f'

This causes an Order (and its set of OrderLine items) to be physically deleted from the system. Note that I'm using SQL Server 2005 as database and my tables are in the Schema called 'Playground'.

Soft Delete

Now for a soft delete we could write in the repository something like this

public void SoftDelete(Order order)
{
    using (ISession session = SessionFactory.OpenSession())
    {
        using (ITransaction tx = session.BeginTransaction())
        {
            order.IsDeleted = true;
            session.Update(order);
            tx.Commit();
        }
    }
}

Note that I have implemented the IsDeleted property of the Order entity such as that it propagates changes to its OrderLine children (only if IsDeleted is set to true).

private bool _isDeleted;
public virtual bool IsDeleted 
{
    get { return _isDeleted; }
    set
    {
        _isDeleted = value;
        if (_isDeleted)
        {
            foreach (var line in OrderLines)
            {
                line.IsDeleted = true;
            }
        }
    } 
}

and the SQL generated by NHibernate is similar to this

NHibernate: UPDATE Playground.Orders SET CustomerName = @p0, 
  OrderDate = @p1, IsDeleted = @p2 WHERE Id = @p3; @p0 = 'IBM', 
  @p1 = '09.04.2008 00:00:00', @p2 = 'True', 
  @p3 = '14bd85fd-0094-446b-8416-953e158747a1'
 
NHibernate: UPDATE Playground.OrderLine SET ProductName = @p0, 
  Amount = @p1, IsDeleted = @p2 WHERE Id = @p3; @p0 = 'Intel Dual Core CPU A', 
  @p1 = '1', @p2 = 'True', @p3 = 'fd274287-22a0-460f-a2f1-42cdb031000c'
 
NHibernate: UPDATE Playground.OrderLine SET ProductName = @p0, 
  Amount = @p1, IsDeleted = @p2 WHERE Id = @p3; @p0 = 'Intel Dual Core CPU B', 
  @p1 = '1', @p2 = 'True', @p3 = 'ec714db7-61e2-4d64-a532-d05da5b8c5af'

As expected the records are not deleted in the database but the order and its order lines are marked as deleted.

Implement and register a DeleteEvent Listener

If you want a more generic approach you'll have to write a DeleteEvent Listener and register it with the NHibernate configuration. You can argue that "soft delete" is a cross cutting concern and as such should not be implemented in the repositories.

This approach is a little bit more involved and you need some intimate knowledge of the inner workings of NHibernate. Events and Event Listeners are a new concept introduced in NHibernate 2.0. Unfortunately this also means that there is not much help or description around at the moment. But the concept is very powerful! What you can do is only limited by your imagination...

Implement the DeleteEvent Listener

A starting point would be to define a class e.g. called MyDeleteEventListener which inherits from the DefaultDeleteEventListener class implemented in NHibernate. Then you override e.g. the DeleteEntity method and define your own "delete" logic.

The code might look similar to this (many thanks for help to Will Shaver)

public class MyDeleteEventListener : 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.IsDeleted = true;
 
            CascadeBeforeDelete(session, persister, entity, entityEntry, transientEntities);
            CascadeAfterDelete(session, persister, entity, transientEntities);
        }
        else
        {
            base.DeleteEntity(session, entity, entityEntry, isCascadeDeleteEnabled,
                              persister, transientEntities);
        }
    }
}

Here I assume that each entity which should be "soft deleted" has to implement a special interface ISoftDeletable which is defined as follows

public interface ISoftDeletable
{
    bool IsDeleted { get; set; }
}

The code in the overriden DeleteEntity method first checks whether the entity implements this interface. If NOT then the call is just forwarded to the base class for default execution (that is the entity will be physically deleted from the system). But if the entity implements the interface then we set its property IsDeleted to true, call the CascadeBeforeDelete and CascadeAfterDelete methods of the base class and are done.

Register the DeleteEvent Listener

Now you have to register this class with your NHibernate configuration

_configuration = new Configuration();
_configuration.Configure();
 
// register the DeleteEvent Listener
_configuration.SetListener(ListenerType.Delete, new MyDeleteEventListener());
 
_configuration.AddAssembly(Assembly.Load(new AssemblyName("DataLayer")));
_sessionFactory = _configuration.BuildSessionFactory();

Testing the DeleteEvent Listener

If you create initial data for your tests like this

private void CreateInitialData()
{
    _order = new Order {CustomerName = "IBM", OrderDate = DateTime.Today};
    _orderLine1 = new OrderLine { Amount = 1, ProductName = "Intel Dual Core CPU A" };
    _orderLine2 = new OrderLine { Amount = 1, ProductName = "Intel Dual Core CPU B" };
    _order.OrderLines.Add(_orderLine1);
    _order.OrderLines.Add(_orderLine2);
 
    using (ISession session = SessionFactory.OpenSession())
    using (ITransaction tx = session.BeginTransaction())
    {
        session.Save(_order);
        tx.Commit();
    }
}

When running a unit test like this

[Test]
public void DeleteEventListener_intercepts_delete_request_for_order()
{
    Order orderToDelete;
    using(ISession session = SessionFactory.OpenSession())
        using (ITransaction tx = session.BeginTransaction())
        {
            orderToDelete = session.Get<Order>(_order.Id);
 
            Assert.IsNotNull(orderToDelete);
            Assert.AreNotSame(_order, orderToDelete);
            Assert.AreEqual(_order.OrderLines.Count, orderToDelete.OrderLines.Count);
 
            session.Delete(orderToDelete);
            tx.Commit();
        }
 
    using (ISession session = SessionFactory.OpenSession())
        Assert.IsNull(session.Get<Order>(orderToDelete.Id));
}

the SQL sent to the database will be similar to this

NHibernate: UPDATE Playground.Orders SET CustomerName = @p0, OrderDate = @p1,
  IsDeleted = @p2 WHERE Id = @p3; @p0 = 'IBM', @p1 = '10.04.2008 00:00:00',
  @p2 = 'True', @p3 = 'd51345d3-234e-4d7f-90cc-00f7767ae15f'
 
NHibernate: UPDATE Playground.OrderLine SET ProductName = @p0, Amount = @p1,
  IsDeleted = @p2 WHERE Id = @p3; @p0 = 'Intel Dual Core CPU A', @p1 = '1',
  @p2 = 'True', @p3 = 'dbfe4468-9c03-4bfe-8520-0a52312b72a8'
 
NHibernate: UPDATE Playground.OrderLine SET ProductName = @p0, Amount = @p1,
  IsDeleted = @p2 WHERE Id = @p3; @p0 = 'Intel Dual Core CPU B', @p1 = '1',
  @p2 = 'True', @p3 = '06561137-2060-46ef-a331-491dfdac705c'

Note that when using a DeleteEvent listener you don't have to implement any cascading logic for the IsDeleted property in the Order (as opposed to the "manual" case where you put your logic in the repository). The IsDeleted property can be simply implemented like this

public virtual bool IsDeleted { get; set; }

Summary

I have show two ways how to fulfill the business requirement not to physically delete an entity from the system but rather mark it as deleted when the application requests deletion of an entity. The first one is done "manually" in the repository for the respective entity or aggregate. The second one is using the concept of event listeners which is new to NHibernate and needs some intimate knowledge on the internal workings of NHibernate.

The second approach has the advantage that the business requirement of "soft deletes" is treated as a cross cutting concern and the solution presented is a generic one.

 Blog Signature Gabriel .


Posted Sat, 06 September 2008 08:25:00 AM by gabriel.schenker
Filed under: database, introduction, event listener

comments powered by Disqus
© NHibernate Community 2024