Soft Deletes

[Updated]

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

image

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 .

Print | posted on Tuesday, April 08, 2008 9:57 PM

Comments on this post

# re: Soft Deletes

Requesting Gravatar...
This one interests me a lot...
Left by pduh on Apr 09, 2008 3:13 AM

# re: Soft Deletes

Requesting Gravatar...
I am wondering how it is possible to inject custom delete rules into this system?
And also wondering how things can be deleted from the database later, when the delete call is being intercepted.
Left by Dennis on Apr 10, 2008 2:53 AM

# re: Soft Deletes

Requesting Gravatar...
@Dennis: you can always physically delete entities in the database just define a "copy" of the respective entity (class, e.g. "Order_Admin") and don't implement the ISoftDeletable interface then a call to delete will not be intercepted. But do you really WANT to do this? This would violate the business requirements...
Left by Gabriel Schenker on Apr 10, 2008 3:29 AM

# re: Soft Deletes

Requesting Gravatar...
Thanks
Left by Kenneth on Apr 10, 2008 8:10 AM

# re: Soft Deletes

Requesting Gravatar...
@Gabriel: Just have different Business requirements where must be able to do both :)
Thanks for your answer.
Still wondering how I can put further rules and get the information out. I could utilize a visitor pattern with a status object, but I don't see a way of getting this status object return to be after calling the initial delete.
Left by Dennis on Apr 10, 2008 1:22 PM

# re: Soft Deletes

Requesting Gravatar...
It is very convenient stuff! We've been using similar approach and also added default filter IsDeleted = false in search methods of repository that use Criteria API. But still we have to add this condition explicitly for HQL queries. Are there any alternatives possible for NH 2.0 using Events\Listeners?
Thanks in advance!
Left by vitalya on Apr 13, 2008 6:23 AM

# re: Soft Deletes

Requesting Gravatar...
Can you put the samples on the Code Repository ?

Thanks in advance!!!
Left by pduh on Apr 13, 2008 8:33 AM

# re: Soft Deletes

Requesting Gravatar...
Has anyone actually tried to run this against the NHibernate 2.0 Alpha1 release? The MyDeleteEventListener inherits from DefaultDeleteEventListener & overrides its DeleteEntity method, but in Alpha1 DefaultDeleteEventListener.DeleteEntity is protected internal.

As a result, the code doesn't even compile. Am I missing something??
Left by jrnail23 on Apr 17, 2008 4:13 AM

# re: Soft Deletes

Requesting Gravatar...
Hi Gabriel,

Firstly thanks for taking the time to compile the FAQ - much appreciated to see some examples of NHibernate in action.

In this case, however, I have to agree with jrnail23's comment: this approach simply will not work against NH2.0 Alpha1...

So.... any thoughts on different ways to approach a 'soft-delete'? (more generally: substituting the default delete behaviour)

Cheers.
Left by Andrew on May 18, 2008 10:24 AM

# re: Soft Deletes

Requesting Gravatar...
OK... think I found my own solution: overridding InvokeDeleteLifecycle. The following appears to do what we want:

protected override bool InvokeDeleteLifecycle(IEventSource session, object entity, IEntityPersister persister) {

if (entity is ISoftDeleteable) {
((ISoftDeleteable) entity).IsDeleted = true;

session.Update(entity);

return true;
}
else {
return base.InvokeDeleteLifecycle(session, entity, persister);
}
}
Left by Andrew on May 18, 2008 11:08 AM

# re: Soft Deletes

Requesting Gravatar...
@jrnail23 and Andrew: the code only works to the trunk of NHibernate. The code in the trunk regarding events has been significantly changed after release of Alpha 1
Left by Gabriel Schenker on May 22, 2008 12:09 AM

# How to undelete?

Requesting Gravatar...
Nice FAQ!!

BTW, do you know if the "where" property on the class item can be easily bypassed in a query? (like a query to list all deleted and not deleted objects)

Nowadays I only know how to bypass it by directly using a ISQLQuery, is this the only way to do it?

ISession s;
ISQLQuery q=s.CreateSQLQuery("SELECT * FROM Orders");
q.AddEntity(typeOf(Order)).List<Order>();
Left by Opera362 on Jul 16, 2008 3:23 AM

# re: Soft Deletes

Requesting Gravatar...
@Opera362: the where statement in the mapping file cannot be bypassed by HQL and/or Criteria API
Left by Gabriel Schenker on Jul 20, 2008 6:09 PM

# re: Soft Deletes

Requesting Gravatar...
Is there a way to intercept lazy loading a collection so isDeleted records are not loaded? Essentially I'm looking for a way to add "where IsDeleted = 0" condition for all my lazy loaded collections.
Left by Dmitriy on Jul 22, 2008 7:19 AM

# re: Soft Deletes

Requesting Gravatar...
Gabriel, what is the best solution for using event listeners after changes in the trunk? Since DeleteEntity is not virtual any more.
Thanks
Left by vitalya on Jul 24, 2008 4:52 AM

# re: Soft Deletes

Requesting Gravatar...
@vitalya: please have a look at the latest code from trunk. Since version 2.0 alpha 1 the DeleteEntity is VIRTUAL and has NOT changed since. I reconfirmed it by inspecting the latest source from the trunk
Left by Gabriel Schenker on Jul 28, 2008 5:35 PM

# re: Soft Deletes

Requesting Gravatar...
How is soft-delete handled in a "many-to-many"-bag relation? The many-to-many relation in the mapping file does not have any element for arbitary sql. Is there a way to get the parent and all NONE-soft-deleted children that is linked via a many-to-many relation?

Thanks
Left by Hannes on Oct 20, 2008 2:54 AM

# re: Soft Deletes

Requesting Gravatar...
Great info. I took the first method a step further by adding a custom class-level attribute called "ShouldSoftDelete". Then, in my generic CRUD repository, I look for that attribute when calling delete, and set isDeleted to true and update, instead of calling delete. Works like a charm.

Let me know if anyone would like to see the code...
Left by Jamie on Nov 18, 2008 2:27 PM

Your comment:

 (will show your gravatar)
 
Please add 1 and 8 and type the answer here: