Soft Deletes With NHibernate

Updated: thanks, Adam

Sometimes we are not allowed to physically delete records from a database. This may be because of some policy, legal obligations, traceability, or whatever. It is sometimes called “soft deleting”. NHibernate offers an elegant solution for this problem.

So, what we want to achieve is:

  • We will use a DELETED column to indicate if a record is deleted (1) or not (0);
  • Only records with DELETED = 0 will be retrieved from the database;
  • If we mark a record for deletion, it will have its DELETED column updated instead.

First, let’s define some interface that characterizes soft deletable entities:

   1: public interface ISoftDeletable
   2: {
   3:     Boolean Deleted
   4:     {
   5:         get;
   6:         set;
   7:     }
   8: }

And a specific implementation:

   1: public class Record : ISoftDeletable
   2: {
   3:     public Record()
   4:     {
   5:         this.Children = new List<Record>();
   6:     }
   7:  
   8:     public virtual Int32 RecordId
   9:     {
  10:         get;
  11:         set;
  12:     }
  13:  
  14:     public virtual String Name
  15:     {
  16:         get;
  17:         set;
  18:     }
  19:  
  20:     public virtual IList<Record> Children
  21:     {
  22:         get;
  23:         protected set;
  24:     }
  25:  
  26:     public virtual Record Parent
  27:     {
  28:         get;
  29:         set;
  30:     }
  31:  
  32:     #region ISoftDeletable Members
  33:  
  34:     public virtual Boolean Deleted
  35:     {
  36:         get;
  37:         set;
  38:     }
  39:  
  40:     #endregion
  41:  
  42:     public override String ToString()
  43:     {
  44:         return (this.Name);
  45:     }
  46: }

Let’s map this using mapping by code:

   1: public class RecordMapping : ClassMapping<Record>
   2: {
   3:     public RecordMapping()
   4:     {
   5:         this.Table("`RECORD`");
   6:         this.Lazy(true);
   7:         this.Where("DELETED = 0");
   8:  
   9:         this.Id(x => x.RecordId, x =>
  10:         {
  11:             x.Column("`RECORD_ID`");
  12:             x.Generator(Generators.HighLow);
  13:         });
  14:  
  15:         this.Property(x => x.Name, x =>
  16:         {
  17:             x.Column("`NAME`");
  18:             x.Length(50);
  19:             x.NotNullable(true);
  20:         });
  21:  
  22:         this.Property(x => x.Deleted, x =>
  23:         {
  24:             x.Column("`DELETED`");
  25:             x.NotNullable(true);
  26:         });
  27:  
  28:         this.ManyToOne(x => x.Parent, x =>
  29:         {
  30:             x.Column("`PARENT_RECORD_ID`");
  31:             x.NotNullable(false);
  32:             x.Lazy(LazyRelation.NoProxy);
  33:             x.Cascade(Cascade.All);
  34:         });
  35:  
  36:         this.Bag(x => x.Children, x =>
  37:         {
  38:             x.Inverse(true);
  39:             x.Cascade(Cascade.All | Cascade.DeleteOrphans);
  40:             x.Where("DELETED = 0");
  41:             x.Key(y =>
  42:             {
  43:                 y.Column("`RECORD_ID`");
  44:                 y.NotNullable(false);
  45:             });
  46:         }, x =>
  47:         {
  48:             x.OneToMany();
  49:         });
  50:     }
  51: }

Did you notice the Where calls on both class and Children properties? Well, they tell NHibernate that a specific restriction will be appended whenever an instance of the Record class is loaded, either directly or through the Children collection (another option would be filters). So, with this, we have set up a loading filter.

What we need now is interception of ISession.Delete calls, to use the Deleted property instead of actually removing the record. We achieve this by using a listener for the PreDelete event:

   1: public class SoftDeletableListener : IPreDeleteEventListener
   2: {
   3:     public void Register(Configuration cfg)
   4:     {
   5:         cfg.EventListeners.PreDeleteEventListeners = new IPreDeleteEventListener[] { this }.Concat(cfg.EventListeners.PreDeleteEventListeners).ToArray();
   6:     }
   7:  
   8:     #region IPreDeleteEventListener Members
   9:  
  10:     public Boolean OnPreDelete(PreDeleteEvent @event)
  11:     {
  12:         ISoftDeletable softDeletable = @event.Entity as ISoftDeletable;
  13:  
  14:         if (softDeletable == null)
  15:         {
  16:             return(true);
  17:         }
  18:  
  19:         EntityEntry entry = @event.Session.GetSessionImplementation().PersistenceContext.GetEntry(@event.Entity);
  20:         entry.Status = Status.Loaded;
  21:  
  22:         softDeletable.Deleted = true;
  23:  
  24:         Object id = @event.Persister.GetIdentifier(@event.Entity, @event.Session.EntityMode);
  25:         Object [] fields = @event.Persister.GetPropertyValues(@event.Entity, @event.Session.EntityMode);
  26:         Object version = @event.Persister.GetVersion(@event.Entity, @event.Session.EntityMode);
  27:  
  28:         @event.Persister.Update(id, fields, new Int32[1], false, fields, version, @event.Entity, null, @event.Session.GetSessionImplementation());
  29:  
  30:         return (true);
  31:     }
  32:  
  33:     #endregion
  34: }

And that’s about it. We use it like this:

   1: Configuration cfg = ...;
   2:  
   3: SoftDeletableListener listener = new SoftDeletableListener();
   4: listener.Register(cfg);
   5:  
   6: ISessionFactory sessionFactory = ...;
   7:  
   8: using (ISession session = sessionFactory.OpenSession())
   9: {
  10:     //only non-deleted records will be retrieved
  11:     Record record = session.Query<Record>().First();
  12:     IEnumerable<Record> children = record.Children.ToList();
  13:  
  14:     session.Delete(record);
  15:     session.Flush();    //an UPDATE will be issued instead of a DELETE
  16: }

As always, hope this is useful to someone! Winking smile

PS - As Adam Bar pointed out, there is another way, which uses sql-delete, and is easier to set up. There's a discussion on StackOverflow, here.

                             

2 Comments

  • Adam:
    Yes, that is definitely another option. I don't use SQL loaders/deleters, but I do use listeners extensively, that's why I chose this. Perhaps that will be the matter for another post.

  • I just adapted this implementation to my project, but it only works for the first entry of every entity. Everytime I want to do delete a second entity, the property Deleted, won't be updated. Could anyone help me?
    BTW, thanks for the post... this is pretty cool!

Add a Comment

As it will appear on the website

Not displayed

Your website