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!
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.