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:
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
Yikes, why use a keyword for the parameter name?
El,
It was ported from java
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?)
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
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.
@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!
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
Carsten,
Take a look at the post about concurrency, and look at optimistic
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.
Carsten,
That is why we have optimistic="dirty"
Hi,
I have some problems with this implementation.
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.
AuditEventListener.Set - if the propertyName is not in the collection: shouldn't it throw?
What do you think? Any ideas about #1?
Thanks
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
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
Revin,
you need to create a child session using:
session.GetSession(EntityMode.Poco) and use that
Thanks for reply Ayende,
Is this some thing that you have in mind?
using (ISession newSession = ev.Source.PersistenceContext.Session.GetSession())
{
}
Where ev is PreUpdateEvent.
Note: Session.GetSession() does not expect any parameter.
Regards,
Revin.
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)
{
}
Everything is OK
Yes
Revin,
That is a bug that I recently fixed in trunk
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.
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
What hook would you use for auditing if you wanted to create entries in a log table rather than setting properties on the object?
Jonathon,
Probably the same hook, and using much the same strategy.
This post on StackOverflow shows how to configure these events: stackoverflow.com/.../how-do-i-implement-change...
I'm having a problem with the set value continuing to persist once the insert occurs. For instance if I have the following code:
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?
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!
I have enity:
class AuditableEntity
{
<auditlog Changes { get; }
}
HBM:
<bag
...
Modified Changes collection do not persists. :(
Why? What I can do to persist that modifed collection?
HBM again:
bag name="Changes" cascade="all" inverse="true" lazy="true" where="EntityName = 'AuditableEntity'"
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
I fully undestand but... prefix 'Pre' (IPreUpdateEventListener) suggest that it should be able to do this. I'll be waiting for post. Thanks.
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