I have implemented listeners to audit changes to tables in my application using IPreUpdateEventListener
and IPreInsertEventListener
and everything works except for my many-to-many relationships that don't have additional data in the joining table (i.e. I don't have a POCO for the joining table).
Each auditable object implements an IAuditable
interface, so the event listener checks to see if a POCO is of type IAuditable
, and if it is it records any changes to the object. Look up tables implement an IAuditableProperty
inteface, so if a property of the IAuditable
POCO is pointing to a lookup table, the changes are recorded in the log for the main POCO.
So, the question is, how should I determine I'm working with a many-to-many collection and record the changes in my audit table?
Edit: I'm using NHibernate 2.1.2.4000
//first two checks for LastUpdated and LastUpdatedBy ommitted for brevity
else if (newState[i] is IAuditable)
{
//Do nothing, these will record themselves separately
}
else if (!(newState[i] is IAuditableProperty) && (newState[i] is IList<object> || newState[i] is ISet))
{
//Do nothing, this is a collection and individual items will update themselves if they are auditable
//I believe this is where my many-to-many values are being lost
}
else if (!isUpdateEvent || !Equals(oldState[i], newState[i]))//Record only modified fields when updating
{
changes.Append(preDatabaseEvent.Persister.PropertyNames[i])
.Append(": ");
if (newState[i] is IAuditableProperty)
{
//Record changes to values in lookup tables
if (isUpdateEvent)
{
changes.Append(((IAuditableProperty)oldState[i]).AuditPropertyValue)
.Append(" => ");
}
changes.Append(((IAuditableProperty)newState[i]).AuditPropertyValue);
}
else
{
//Record changes for primitive values
if(isUpdateEvent)
{
changes.Append(oldState[i])
.Append(" => ");
}
changes.Append(newState[i]);
}
changes.AppendLine();
}
The reason that this won't fire is because the collections haven't changed, i.e. they're still the same instance of ICollection that was there before, however the content of the collections have changed.
I've looked for this myself, and the event listeners do not handle this situation. This may have been fixed for v3.0 (but don't quote me on that). There are a couple of non ideal workarounds:
1)Put a property on the object which makes a string representation of the collection for the purposes of auditing.
2)Make the items in the collection implement the interface so they are audited individually.
Edit: There is a third option:
"Rather than a many-to-many, I have a many-to-one going to the joining table, and then a one-to-many coming from it into the property table. I hide the joining table POCO behind the logic of each of the ends of the many-to-many joins, but still have to implement the object and all the interfaces on it."
It turns out that there actually is a way to do this via Event Listeners without having to expose the joining tables. You just have to make your event listener implement IPostCollectionRecreateEventListener or IPreCollectionRecreateEventListener. Based on my testing these events get fired for changed collections whenever the session is flushed. Here's my event listener code for the PostRecreateCollection method.
public void OnPostRecreateCollection(PostCollectionRecreateEvent @event)
{
var session = @event.Session.GetSession(EntityMode.Poco);
var propertyBeingUpdated = @event.Session.PersistenceContext.GetCollectionEntry(@event.Collection).CurrentPersister.CollectionMetadata.Role;
var newCollectionString = @event.Collection.ToString();
var oldCollection = (@event.Collection.StoredSnapshot as IList<object>).Select(o => o.ToString()).ToList();
var oldCollectionString = string.Join(", ",oldCollection.ToArray());
if (newCollectionString == oldCollectionString || (string.IsNullOrEmpty(newCollectionString) && string.IsNullOrEmpty(oldCollectionString)))
return;
User currentUser = GetLoggedInUser(session);
session.Save(new Audit
{
EntityName = @event.AffectedOwnerOrNull.GetType().Name,
EntityId = (int)@event.AffectedOwnerIdOrNull,
PropertyName = propertyBeingUpdated,
AuditType = "Collection Modified",
EventDate = DateTime.Now,
NewValue = newCollectionString,
OldValue = oldCollectionString,
AuditedBy = Environment.UserName,
User = currentUser
});
}
The most tricky part is getting the name of the collection being updated. You have to chain your way through the PersistenceContext to get the Persister for the collection which gives you access to it's meta data.
Because none of these events or listeners are documented I don't know if this event gets thrown in other situations besides flush, so there's a potential it could create false audit entries. I plan to do some further research in that area.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With