NHibernate Auditing v3 - Poor Man's Envers
First, let me explain the title of this post. The Hibernate folks – you know, that NHibernate knock off written in the Java (pronounced “ex em el”) programming language – have a project called Envers. Among other things, It audits changes to entities, then allows you to easily retrieve the entity as it was at any previous point in time.
Well, Simon Duduica is porting this over to .NET and NHibernate, and he’s making some AMAZING progress. On June 28th, he shared this news with us on the NH Contrib development group:
Hi everybody,
I have news regarding Envers.NET. I've commited a version that works in basic tests for CUD operations, with entities that have relationships between them, also with entities that are not audited. To make things work I had to make two small modifications of NHibernate, both modifications were tested running all NHibernate unit tests and they all passed. I already sent the first modification to Fabio and the second I will send this evening. I would like to thank Tuna for helping me out with good advices when I was stuck :)
So, on to the topic of this post. For NHibernate 3.0 Cookbook, I’ve included a section that explains how to use NHibernate to generate audit triggers. Originally, I had planned to use the code from my previous blog post on the topic, but I didn’t like its structure. I also didn’t want to include all that plumbing code in the printed book. Instead, I’ve rewritten and contributed the “framework” code to uNHAddIns. The “how-to use it” is explained in the book, so I won’t explain it here.
Today, I was writing an integration test for this contribution, and thought the idea was worth sharing. I have a simple Cat class:
When I do anything to this cat, in addition to the normal INSERT, UPDATE, or DELETE, a database trigger records that action in a table called CatAudit:
I wanted an easy way to investigate the contents of this table to prove that my audit triggers worked. Here’s what I came up with, along with help from Jose Romaniello (@jfroma). First, I created a class to match this table:
Next, I mapped it, made it readonly and excluded it from hbm2ddl with this mapping:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="uNhAddIns.Test" namespace="uNhAddIns.Test.Audit.TriggerGenerator"> <typedef class="NHibernate.Type.EnumStringType`1[[uNhAddIns.Audit.TriggerGenerator.TriggerActions, uNhAddIns]], NHibernate" name="triggerActions" /> <class name="CatAudit" mutable="false" schema-action="none"> <composite-id> <key-property name="Id" /> <key-property name="AuditUser" /> <key-property name="AuditTimestamp" /> </composite-id> <property name="Color"/> <property name="AuditOperation" type="triggerActions" /> </class> </hibernate-mapping>
I made it readonly by setting mutable="false" and excluded it from hbm2ddl with schema-action="none". That’s it!
By the way, the <typedef> along with type="triggerActions" just tells NHibernate I've stored my TriggerActions enum values as strings, not numbers.