Ayende @ Rahien

Hi!
My name is Ayende Rahien
Founder of Hibernating Rhinos LTD and RavenDB.
You can reach me by phone or email:

ayende@ayende.com

, @ Q c

Posts: 5,949 | Comments: 44,547

filter by tags archive

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

Yikes, why use a keyword for the parameter name?

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

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

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

@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

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

Carsten,

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

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

Carsten,

That is why we have optimistic="dirty"

mg
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

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

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

Revin,

you need to create a child session using:

session.GetSession(EntityMode.Poco) and use that

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

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

Revin,

That is a bug that I recently fixed in trunk

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

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

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

Jonathon,

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

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

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

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

HBM again:

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

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

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

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

Comment preview

Comments have been closed on this topic.

FUTURE POSTS

No future posts left, oh my!

RECENT SERIES

  1. The RavenDB Comic Strip (3):
    28 May 2015 - Part III – High availability & sleeping soundly
  2. Special Offer (2):
    27 May 2015 - 29% discount for all our products
  3. RavenDB Sharding (3):
    22 May 2015 - Adding a new shard to an existing cluster, splitting the shard
  4. Challenge (45):
    28 Apr 2015 - What is the meaning of this change?
  5. Interview question (2):
    30 Mar 2015 - fix the index
View all series

RECENT COMMENTS

Syndication

Main feed Feed Stats
Comments feed   Comments Feed Stats