NHibernate 2.1 and Collection Event Listeners
In a previous post, I talked about cascading deletes being a new feature introduced by NHibernate 2.0. If you haven’t heard about this before, then you’d probably be interested to read about it first.
Cascading deletes are all great if your database of choice supports CASCADE DELETE foreign key constraints. But what if it doesn’t provide this feature or, as in my case, the database in question does support this feature but the DBA’s don’t want anything to do with it? In case of a parent domain object having a collection of many child objects, you still might want to have a one-shot delete capability instead of having separate DELETE statements for each child record.
The newly released NHibernate 2.1 (congratulations to the entire team for their efforts and hard work) comes to the rescue, which introduces a couple of new event listeners that deal with collections.
First we need an example. Suppose we are building an auction web site and the domain has a class called Item which in turn has a collection of Bids.
public class Item { private ISet<Bid> _bids; public Int64 Id { get; private set; } ... } public class Bid { public Double Amount { get; private set; } public Sting Code { get; private set; } ... }
The mapping for these classes looks something like this:
<class name="MyAuction.Item, MyAuction" table="Item"> <id name="Id" type="Int64" unsaved-value="-1"> <column name="Id" sql-type="integer"/> <generator class="native"/> </id> ... <set name="Bids" access="field.camelcase-underscore" lazy="false" cascade="all-delete-orphan" inverse="true" optimistic-lock="false"> <key> <column name="ItemId"/> </key> <one-to-many class="MyAuction.Bid, MyAuction"/> </set> </class> <class name="MyAuction.Bid, MyAuction" table="Bid"> <composite-id> <key-property name="ItemId" column="ItemId" type="Int64"/> <key-property name="Code" column="Code" type="String"/> </composite-id> ... </class>
Just to give you a general idea of the situation here. Now suppose we want to delete a quite popular Item object which has a numerous amount of Bids. Because the collection of Bids is mapped as inverse, NHibernate will remove every record for a Bid with a separate DELETE statement for each row.
DELETE FROM Bid WHERE ItemId=@p0 AND Code=@p1 ;@p0 = 2, @p1 = 'F1001' DELETE FROM Bid WHERE ItemId=@p0 AND Code=@p1 ;@p0 = 2, @p1 = 'F1002' DELETE FROM Bid WHERE ItemId=@p0 AND Code=@p1 ;@p0 = 2, @p1 = 'F1003' ... DELETE FROM Item WHERE Id = @p0; @p0 = 2
We could solve this by creating a collection event listener. The first thing we have to do is figure out how to issue a one-shot delete instead of those separate DELETE statements.
public interface IOneShotDeleteHandler { Type ForEntity(); Type[] ForChildEntities(); void GiveItAShot(ISession session, Object entity); } public class OneShotDeleteHandlerForItem : IOneShotDeleteHandler { public Type ForEntity() { return typeof(Item); } public Type[] ForChildEntities() { return new[] { typeof(Bid) }; } public void GiveItAShot(ISession session, Object entity) { var item = (Item)entity; session.CreateQuery("delete Bid where ItemId = :itemId") .SetInt64("itemId", item.Id) .ExecuteUpdate(); } }
We created an IOneShotDeleteHandler interface with one implementation for the Item class. The most notable aspect of this implementation is the use of the HQL delete statement that removes all Bids for a particular Item.
Next step is to create a collection event listener that implements the IPreCollectionRemoveEventListener interface.
public interface IEventListener { void ConfigureFor(Configuration configuration); } public class CollectionRemoveEventListener : IPreCollectionRemoveEventListener, IEventListener { private readonly IEnumerable<IOneShotDeleteHandler> _oneShotDeleteHandlers; public CollectionRemoveEventListener( IEnumerable<IOneShotDeleteHandler> oneShotDeleteHandlers) { _oneShotDeleteHandlers = oneShotDeleteHandlers; } public void OnPreRemoveCollection( PreCollectionRemoveEvent @event) { var affectedOwner = @event.AffectedOwnerOrNull; if(null == affectedOwner) return; var oneShotDeleteHandler = _oneShotDeleteHandlers.SingleOrDefault(handler => handler.ForEntity() == affectedOwner.GetType()); if(null == oneShotDeleteHandler) return; oneShotDeleteHandler .GiveItAShot(@event.Session, affectedOwner); } public void ConfigureFor(Configuration configuration) { configuration .SetListener(ListenerType.PreCollectionRemove, this); } }
Don’t worry about the IEventListener interface. Its just there for registering all NHibernate event listeners in an IoC container.By doing so, it enables us to inject a collection of IOneShotDeleteHandler objects into the constructor of our event listener. When the OnPreRemoveCollection method is called, we simply lookup whether there’s a handler available for the type of entity that’s going to be deleted and give it a shot at removing its child collection in one sweep.
Now we only have to register this event listener:
var eventListeners = _dependencyContainer .ResolveAll<IEventListener>(); eventListeners.ForEach(eventListener => eventListener .ConfigureFor(configuration));
Now, if we would use this ‘as is’, NHibernate will give us the following error:
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
This is due to the fact that the collection event listener does its work and deletes all Bids by executing the HQL statement, but NHibernate still tries to issue a DELETE statement for each Bid. This is the result of the all-delete-orphan cascading rule we imposed in the mapping. We could reduce it to save-update, but then no individual DELETE statements are executed when a singe Bid is removed from the collection. Now what?
Well, we could provide a regular delete event listener that allows individual DELETE statements for Bid entities as long as their parent Item is not removed.
public class DeleteEventListener : DefaultDeleteEventListener, IEventListener { private readonly IEnumerable<IOneShotDeleteHandler> _oneShotDeleteHandlers; public DeleteEventListener( IEnumerable<IOneShotDeleteHandler> oneShotDeleteHandlers) { _oneShotDeleteHandlers = oneShotDeleteHandlers; } protected override void DeleteEntity( IEventSource session, object entity, EntityEntry entityEntry, Boolean isCascadeDeleteEnabled, IEntityPersister persister, ISet transientEntities) { var oneShotDeleteHandler = _oneShotDeleteHandlers .SingleOrDefault(handler => handler.ForChildEntities() .Contains(entity.GetType())); if(null == oneShotDeleteHandler || !IsParentAlsoDeletedIn( session.PersistenceContext, oneShotDeleteHandler.ForEntity()) ) { base.DeleteEntity(session, entity, entityEntry, isCascadeDeleteEnabled, persister, transientEntities); return; } CascadeBeforeDelete(session, persister, entity, entityEntry, transientEntities); CascadeAfterDelete(session, persister, entity, transientEntities); } public void ConfigureFor(Configuration configuration) { configuration.SetListener(ListenerType.Delete, this); } private static Boolean IsParentAlsoDeletedIn( IPersistenceContext persistenceContext, Type typeOfParent) { foreach(DictionaryEntry entry in persistenceContext.EntityEntries) { if(typeOfParent != entry.Key.GetType()) continue; var entityEntry = (EntityEntry)entry.Value; if(Status.Deleted == entityEntry.Status) return true; } return false; } }
With both event listeners registered, deleting a single Bid results in a single DELETE statement as one would expect:
DELETE FROM Bid WHERE ItemId=@p0 AND Code=@p1 ;@p0 = 2, @p1 = 'F1002'
and removing an entire Item now results in a one-shot delete for all Bids:
DELETE FROM Bid WHERE ItemId=@p0; @p0 = 2 DELETE FROM Item WHERE Id = @p0; @p0 = 2
Make sure you use this solution for one-shot deletes wisely and only if you have to. If you can use the CASCADE DELETE foreign key constraints, then by all means, this is the preferred option. If not, only resort to this kind of solution only if you must and that you can prove that its going to give you a tremendous performance benefit. Also take a look at the batching support that NHibernate provides (at the moment only SQL Server and Oracle are supported).
Till next time
I’m triying do it but not localize de “ExecuteUpdate”. What Reference must added for this?
Thanks
Are you using NH 2.1 or higher?
Yes, the Nh 2.2 version.
Mmmm, that’s odd. It should be there. Are you sure that you’re using the HQL API?