Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Audit many-to-many relationship in NHibernate

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();
}
like image 385
Kendrick Avatar asked Dec 24 '10 15:12

Kendrick


2 Answers

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."

like image 173
Gunner Avatar answered Oct 20 '22 06:10

Gunner


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.

like image 43
AndrewSwerlick Avatar answered Oct 20 '22 08:10

AndrewSwerlick