Intercepting NHibernate to Handle Additional Database Work
Today I needed to take addition database action when an entity is saved or deleted. A trigger might have been an option, but I try to limit the use of triggers. NHibernate to the rescue! Since the code was already using NHibernate it was an easy choice. There are a couple of options here. First is to use a stored procedure for the insert. However, that requires not using identity columns and wouldn’t work in this case. There is the ILifecycle interface, but it is deprecated and doesn’t have after-save hooks. Which leaves the IInterceptor interface.
Sub-classing EmptyInterceptor, I overrode the needed methods. If the entity is the one I’m concerned with, I store it or it’s data for later use in a queue collection. In the PostFlush method the entities stored in the queue will have their Identity set and can be used to set the properties for the additional work. Also, the deleted object will have been deleted which is important. I’m not sure I needed lock the queue objects, but I guess there is a change the session could be used on multiple threads and to be save they are locked.
The named queries are stored in the MyEntity Mapping file using the sql-query element.
public class MyEntityInterceptor : EmptyInterceptor
{
private ISession session;
private readonly Queue<MyEntity> saves = new Queue<MyEntity>();
private readonly Queue<int> deletes = new Queue<int>();
public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, global::NHibernate.Type.IType[] types)
{
var myEntity = entity as MyEntity;
if (myEntity != null)
saves.Enqueue(myEntity);
return true;
}
public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, global::NHibernate.Type.IType[] types)
{
var myEntity = entity as MyEntity;
if (myEntity != null)
lock (deletes)
if (!deletes.Contains(myEntity.ParentId))
deletes.Enqueue(myEntity.ParentId);
}
public override void PostFlush(ICollection entities)
{
ExecuteSaves();
ExecuteDeletes();
}
private void ExecuteSaves()
{
lock (saves)
while (saves.Count > 0)
{
var myEntity = saves.Dequeue();
session.GetNamedQuery("SqlToExecuteForSaves")
.SetInt32("Id", myEntity.Id)
.ExecuteUpdate();
}
}
private void ExecuteDeletes()
{
lock (deletes)
while (deletes.Count > 0)
{
var id = deletes.Dequeue();
session.GetNamedQuery("SqlToExecuteForDeletes")
.SetInt32("Id", id)
.ExecuteUpdate();
}
}
public override void SetSession(ISession session)
{
session = session;
}
}
public class MyEntity
{
public int ParentId;
public int Id;
}
Since it’s using the ISession and IQuery interface, unit testing was easy.
The code is also using the NHibernateFacility from the CastleProject. Which makes adding interceptors as easy as adding them to the container. The naming is important as that’s how the facility looks up the interceptors. The first will be used for all session factories unless there is on for the specific factory as the second option shows.
container.Register(Component.For<IInterceptor>()
.ImplementedBy<MyEntityInterceptor>()
.Named("nhibernate.session.interceptor"));
container.Register(Component.For<IInterceptor>()
.ImplementedBy<MyEntityInterceptor>()
.Named("nhibernate.session.interceptor.MyOtherFactoryAlias"));
Webmentions
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: