zaterdag 5 juli 2008

NHibernate IInterceptor: an AuditInterceptor

As I was playing around with NHibernate today, I came accross a rather inconvenient problem. :).

Let me first explain what I wanted to achieve:
For every domain object that I save, I want to persist in the database when the entity has been created, when it has been last updated and by whom. Nothing special, just regular audit-information.

To make this all possible, I've created the following classes / interfaces:

  • IAuditable interface




  • AuditableEntity interface



I think this is pretty straightforward and doesn't require any further explanation.
Then, I continued with creating an NHibernate interceptor which would set the Created and Updated dates. (I could also used the ILifecycle interface instead, but this meant that I would have a dependency to the NHibernate assembly in my 'domain classes assembly', and I don't like that. In fact, the ILifecycle interface has been deprecated for exactly that reason).

This is an extract from my AuditInterceptor which would perform the task I wanted (at least, I thought so ... ).
(Note that my AuditInterceptor is NOT in the same assembly where the IAuditable, AuditableEntity and other domain base class reside in. This would create a dependency from my base classes to NHibernate and again, I hate this :) ).

The AuditInterceptor (snippet):


As you can see, it is very simple: I only had to implement 2 methods of the IInterceptor interface:

  • OnSave, which is called when an entity is saved for the first time in the database (INSERT)

  • OnFlushDirty, which is called when an existing entity is dirty and has to be updated
What I do, is check whether the entity that is to be saved implements the IAuditable interface, and if so, I just set the necessary properties (Created and Updated) to the appropriate values (the current DateTime).

Easy enough, simple, understandable and clean... If only this would work...
During testing, I got the following exception:

  ----> System.Data.SqlTypes.SqlTypeException : SqlDateTime overflow. 
Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object[] fields,
Boolean[] notNull, SqlCommandInfo sql, Object obj, ISessionImplementor session)

As it turns out, NHibernate doesn't 'see' the changes you make to the entity parameter that is passed to the Interceptor methods:



You can however, change the values that are in the state array parameter. Then NHibernate will correctly persist the changes.

But, I do not like to 'hard-code' property names as strings for obvious reasons (if you change a property, the compiler will not detect that you should change your 'hardcoded property name string', etc...).

Anyway, in order to get my interceptor to work, I have no other choice then messing around with the propertyNames[] and state[] parameters.
In order to get rid of the 'weak-typing', I added a little bit more code.
So, now my classes look like this:

  • IAuditable interface



  • AuditableEntity class



  • AuditInterceptor


This solution is, IMHO, elegant enough to live with, and it works.

However, maybe someone else has a better, more elegant solution for this ? If so, I'd like to hear from you ...

7 opmerkingen:

Jan Van Ryswyck zei

We hav been strugling with the same issue (http://elegantcode.com/2008/05/15/implementing-nhibernate-interceptors/). The solution we came up with is almost the same as the one you are proposing. Wouldn't know for a better solution. Maybe the new event listeners in NH 2.0 can bring more to the table.

Frederik Gheysels zei

Hi.
I also don't think that there's another solution for the problem.

Interesting blog you have there, btw.

Henrik zei

You can use the timestamp or version
http://www.hibernate.org/hib_docs/nhibernate/html_single/#mapping-declaration-timestamp

From nhibernate.

Otherwise, why have a version and timestamp, both at the same time and not just one of them? Timestamp = versioning, right? Unless it's a business concept; the version, but then it seems misplaced in IAuditable.

Frederik Gheysels zei

@henrik
Hmm, in a way, you're right.
The Updated property is just a regular DateTime which indicates when an entity has last changed.

The Version property is there for versioning / optimistic concurrency. I indeed use the version element in my NHibernate mapping file to implement this versioning.

I choose to not use the Updated property for the versioning-mechanism, since I think that using a datetime for this purpose could be a little bit unsafe in some situations. (2 updates occurring at exactly the same time).
Also, the supported types for the Version element are Int16, Int32, Int64, Ticks and Timespan.

The Timespan element in NHibernate could be interesting, especcially since you have the option to specify that it should be generated by the DB. However, I think that this timestamp is not implemented as a regular DateTime, but as a Timestamp ?

Frederik Gheysels zei

Actually henrik, your post made me think ...

Actually, the AuditInterceptor should only be responsible for saving who has created the entity, and who has last updated it. (So, only the LastUpdatedBy and CreatedBy properties should be set via the interceptor).
The reason why I think that, is that the clock of the (database) server should be used to determine the date and time when the entity has been created and updated.

A way to implement this, would be to use the Timestamp element in your NHibernate mapping file to get the LastUpdatedDateTime of the entity (but, this means you cannot use the Version element anymore).
For the Created property, we could use a datetime-column in the database which has a DEFAULT constraint on it.

But, maybe there are people out there with a different opinion that they'd like to share. :)

Anoniem zei
Deze reactie is verwijderd door een blogbeheerder.
Noctris zei

Might be a bit late to join in this conversation but we handled this slightly different after running into a whole bunch of trouble using your method ( although i must admit that afterwards, we found out that these were related to something we are still struggling with today)

We audit the following things on every of our records:

reccreated, reccreater, recmodified, recmodifier, recdeleted, recdeleter and have a flag field "deleted" since we only do logical deletes with nhibernate. When we tryed to implement this through interceptors, we got strange errors where nhibernate seemed to fire the interceptor twice and the second time, it did not understand where the change ( audit info) happened ( don't remember the exact error but can look it up)

We then chose to do these things using events:

IPreUpdateEventListener, IPreInsertEventListener, IPreDeleteEventListener, IFlushEntityEventListener

and that worked out a lot better.. The only sad thing is that we still have no way to insert the delete information in a clean way. When you alter an entity in the PreDelete stage, Nhibernate pretends to do this but then undeletes (yes .. it logically undeletes the record) at the next transaction.. :s

So anyone that has a way to inser audit information in the IPreDelete Event is very welcome ;-) (or could i do a sessionless update ?)