Ayende @ Rahien

Refunds available at head office

NHibernate IPreUpdateEventListener & IPreInsertEventListener

NHibernate’s listeners architecture bring with it a lot of power to the game, but understanding how to use it some of the listeners properly may require some additional knowledge. In this post, I want to talk specifically about the pre update hooks that NHibernate provides.

Those allow us to execute our custom logic before the update / insert is sent to the database. On the face of it, it seems like a trivial task, but there are some subtleties that we need to consider when we use them.

Those hooks run awfully late in the processing pipeline, that is part of what make them so useful, but because they run so late, when we use them, we have to be aware to what we are doing with them and how it impacts the rest of the application.

Those two interface define only one method each:

bool OnPreUpdate(PreUpdateEvent @event) and bool OnPreInsert(PreInsertEvent @event), respectively.

Each of those accept an event parameter, which looks like this:

image

Notice that we have two representations of the entity in the event parameter. One is the entity instance, located in the Entity property, but the second is the dehydrated entity state, which is located in the State property.

In NHibernate, when we talk about the state of an entity we usually mean the values that we loaded or saved from the database, not the entity instance itself. Indeed, the State property is an array that contains the parameters that we will push into the ADO.Net Command that will be executed as soon as the event listener finish running.

Updating the state array is a little bit annoying, since we have to go through the persister to find appropriate index in the state array, but that is easy enough.

Here comes the subtlety, however. We cannot just update the entity state. The reason for that is quite simple, the entity state was extracted from the entity and place in the entity state, any change that we make to the entity state would not be reflected in the entity itself. That may cause the database row and the entity instance to go out of sync, and make cause a whole bunch of really nasty problems that you wouldn’t know where to begin debugging.

You have to update both the entity and the entity state in these two event listeners (this is not necessarily the case in other listeners, by the way). Here is a simple example of using these event listeners:

public class AuditEventListener : IPreUpdateEventListener, IPreInsertEventListener
{
	public bool OnPreUpdate(PreUpdateEvent @event)
	{
		var audit = @event.Entity as IHaveAuditInformation;
		if (audit == null)
			return false;

		var time = DateTime.Now;
		var name = WindowsIdentity.GetCurrent().Name;

		Set(@event.Persister, @event.State, "UpdatedAt", time);
		Set(@event.Persister, @event.State, "UpdatedBy", name);

		audit.UpdatedAt = time;
		audit.UpdatedBy = name;

		return false;
	}

	public bool OnPreInsert(PreInsertEvent @event)
	{
		var audit = @event.Entity as IHaveAuditInformation;
		if (audit == null)
			return false;


		var time = DateTime.Now;
		var name = WindowsIdentity.GetCurrent().Name;

		Set(@event.Persister, @event.State, "CreatedAt", time);
		Set(@event.Persister, @event.State, "UpdatedAt", time);
		Set(@event.Persister, @event.State, "CreatedBy", name);
		Set(@event.Persister, @event.State, "UpdatedBy", name);

		audit.CreatedAt = time;
		audit.CreatedBy = name;
		audit.UpdatedAt = time;
		audit.UpdatedBy = name;

		return false;
	}

	private void Set(IEntityPersister persister, object[] state, string propertyName, object value)
	{
		var index = Array.IndexOf(persister.PropertyNames, propertyName);
		if (index == -1)
			return;
		state[index] = value;
	}
}

And the result is pretty neat, I must say.

Comments

El Guapo
04/29/2009 03:00 PM by
El Guapo

Yikes, why use a keyword for the parameter name?

Ayende Rahien
04/29/2009 03:03 PM by
Ayende Rahien

El,

It was ported from java

huey
04/29/2009 04:04 PM by
huey

Just to clarify, this will work in conjunction with IUserType stuff too? Like the Entity object is just using properties like you would anywhere else in code, but the State object will end up calling the IUserType set method?

I have the exact same thing on most columns (create date/time and modify date/time) but have to use a custom UserType because the database expects funky format. This would be pretty slick to remove that transaction stamp stuff from all over the code.

(unrelated but I used HQL recently from always using Critiera and noticed the UserType isn't used for setting a query parameter ... is HQL not able to deduce what it should type a parameter as with what it is being compared to?)

Ayende Rahien
04/29/2009 06:56 PM by
Ayende Rahien

Huey,

IPreXyz are run after the user type has been run.

And yes, they can work in concrete

About the second issue, it should work

Chris Smith
04/29/2009 07:17 PM by
Chris Smith

The event handlers are cool, especially for managing our legacy audit stuff. We use the same thing to enlist a stored procedure in the current NH transaction. The stored procedure copies the row to a second table prior to the update/delete and after the insert, along with the user and timestamp. It allows us to keep a full history of every row change made.

The stored procedure integration is ugly but once the legacy stuff is gone, it'll be replaced with something far more elegant.

huey
04/29/2009 08:14 PM by
huey

@Ayende

Thanks, I'll have to check this out. Almost all of the tables in our ERP system have log fields for create/modify (user/program/date/time). It is really just noise putting it all over code and I definitely could miss spots. If this can work it would be great and actually make things better as well as simpler.

The 2nd issue isn't a big deal. I have posted about it on SO ( stackoverflow.com/.../nhibernate-hql-and-userty...) but can work around it easy using ICriteria so haven't digged deeper. I don't mean to use your blog as support!

Carsten Hess
04/30/2009 11:34 AM by
Carsten Hess

Is it possible to delete parameters in these listeners as well as updating them ?

I'm trying to obtain a merge functionality in the database when 2 or more caseworkers updates different columns in the same row in a table.

I have a situation where data from the UI layer comes back to my server in specific agents which updates my domainmodel (by using a new session to load my entities in the domainmodel on demand). The data from the agent uncritically updates all fields it contains - whether they were changed in the UI or not. I use "dynamic-update".

The problem is that because data are updated on my entities regardless of changes, nhibernate will mark a property a dirty both when the change came from the UI or when the change was in the underlying database (made by another caseworker).

I would like nhibernate not to update the column unless the change came from the UI saving data in this session.

My agents now have information on which fields are dirty from the UI point of view, so I COULD update the domainmodel properties conditionally now - but searching for a more generic solution.

/Carsten

Ayende Rahien
04/30/2009 04:15 PM by
Ayende Rahien

Carsten,

Take a look at the post about concurrency, and look at optimistic

Carsten Hess
05/01/2009 06:30 AM by
Carsten Hess

Hi Ayende - I know optimistic locking, but I as you write your self : "..It basically states that if we detect a change in the entity, we cannot update it".

I'm interested in a silent merge instead of a StaleStateException.

Ayende Rahien
05/01/2009 09:12 AM by
Ayende Rahien

Carsten,

That is why we have optimistic="dirty"

mg
05/04/2009 08:50 PM by
mg

Hi,

I have some problems with this implementation.

  1. If you set CreatedAt and CreatedBy as not-null properties, as you would normally do, the insert event is fired after the nullability check by NH (like two lines earlier: see AbstractSaveEventListener.PerformSaveOrReplicate), so we get an error before the listener has a chance to update properties.

  2. AuditEventListener.Set - if the propertyName is not in the collection: shouldn't it throw?

What do you think? Any ideas about #1?

Thanks

Ayende Rahien
05/05/2009 06:33 AM by
Ayende Rahien

mg,

1) you are right, but trying to have a fix for that would be quite a challenge. The fix is to remove NH nullability check, which I agree is not ideal.

2) you can say it should throw, or that it is optional property, that depends on you

Revin Hart
05/19/2009 10:34 PM by
Revin Hart

Hi,

For every changes in the entity, I need to insert a new row in table AuditLog containing the old and new value. Is it possible at this stage (OnPreUpdate) to create a new AuditLog instance? How can I get the NHibernate Session?

Thanks

Ayende Rahien
05/19/2009 10:42 PM by
Ayende Rahien

Revin,

you need to create a child session using:

session.GetSession(EntityMode.Poco) and use that

Revin Hart
05/20/2009 10:03 PM by
Revin Hart

Thanks for reply Ayende,

Is this some thing that you have in mind?

using (ISession newSession = ev.Source.PersistenceContext.Session.GetSession())

{

foreach (ScheduleChangeLog changeLog in logs)

{

    changeLog.UpdatedBy = Username;

    newSession.Save(changeLog);

    newSession.Flush();

}

}

Where ev is PreUpdateEvent.

Note: Session.GetSession() does not expect any parameter.

Regards,

Revin.

Revin Hart
05/21/2009 05:16 AM by
Revin Hart

With the above code, I receive exception: Session is already closed when closing the original session.

So I changed the code into:

ISession newSession = ev.Source.PersistenceContext.Session.GetSession();

foreach (ScheduleChangeLog changeLog in logs)

{

changeLog.UpdatedBy = Username;

newSession.Save(changeLog);

}

Everything is OK

Ayende Rahien
05/21/2009 05:26 AM by
Ayende Rahien

Revin,

That is a bug that I recently fixed in trunk

Revin Hart
05/21/2009 10:19 PM by
Revin Hart

Hi Ayende,

The nature of the bug:

When closing the child session, the parent session is closed as well.

They are not independent.

So your fix is to make them independent. Right?

Hence the latest code that I proposed is working and no side effect at all

as parent and child sessions are closed later when closing parent session.

Just for my information:

Normally how do I get the bug fixed in NHibernate?

When is the next release of NHibernate?

Regards,

Revin.

Ayende Rahien
05/21/2009 10:22 PM by
Ayende Rahien

Revin,

Yes, that is the issue.

And yes, that is fixed.

We hope to release alpha 3 soon (days).

For bugs, you report them on the Jira

Jonathon Rossi
05/25/2009 11:33 AM by
Jonathon Rossi

What hook would you use for auditing if you wanted to create entries in a log table rather than setting properties on the object?

Ayende Rahien
05/25/2009 01:02 PM by
Ayende Rahien

Jonathon,

Probably the same hook, and using much the same strategy.

Jason Sirota
06/19/2009 02:46 AM by
Jason Sirota

I'm having a problem with the set value continuing to persist once the insert occurs. For instance if I have the following code:

        var myObj = new MyObject

        {

            MemberId = "123456",

            SystemId = "XXXX"

        };


        using (ISession session = Helper.OpenSession())

        {

            session.Save(myObj);

            Assert.IsTrue(myObj.ID> 0);

            Assert.IsNotNull(myObj.CreatedAt); //This fails

        }


        myObj.SystemId = "YYYY";


        using (ISession session = Helper.OpenSession())

        {

            session.SaveOrUpdate(myObj);

            Assert.IsTrue(myObj.ID> 0);

            Assert.IsNotNull(myObj.CreatedAt); 

        }

If I take out the failed call, to see what's happening, I see that the value is inserted intot he database on the first call, but it overwritten on the second call because the entity itself doesn't have the value?

Jason Sirota
06/19/2009 02:53 AM by
Jason Sirota

Never mind, I was trying to write the Entity using reflection against the base type so I didn't have to expose the nullable datetimes to the API, but my reflection code was bugged. Solved!

dario-g
06/25/2009 01:38 PM by
dario-g

I have enity:

class AuditableEntity

{

IList

<auditlog Changes { get; }

}

HBM:

<bag
...

Modified Changes collection do not persists. :(

Why? What I can do to persist that modifed collection?

dario-g
06/25/2009 01:41 PM by
dario-g

HBM again:

bag name="Changes" cascade="all" inverse="true" lazy="true" where="EntityName = 'AuditableEntity'"

Ayende Rahien
06/25/2009 01:44 PM by
Ayende Rahien

As I mentioned in the blog post, this is too late in the game for modifying the entity, you have to create a new entity for that, I'll post something about it soon

dario-g
06/25/2009 01:55 PM by
dario-g

I fully undestand but... prefix 'Pre' (IPreUpdateEventListener) suggest that it should be able to do this. I'll be waiting for post. Thanks.

dario-g
06/25/2009 03:11 PM by
dario-g

ok, I try to do something like Revin do but new autidinfo entities are not stored in database :(

@Revin: How you do this? :)

my email: developer@dario-g.com

Comments have been closed on this topic.