An NHibernate audit trail that doesn't cause “collection was not processed by flush” errors

前端 未结 4 1220
情深已故
情深已故 2020-12-31 18:14

Ayende has an article about how to implement a simple audit trail for NHibernate (here) using event handlers.

Unfortunately, as can be seen in the comments, his impl

相关标签:
4条回答
  • 2020-12-31 18:42

    The fix should be the following. Create a new event listener class and derive it from NHibernate.Event.Default.DefaultFlushEventListener:

    [Serializable] 
    public class FixedDefaultFlushEventListener: DefaultFlushEventListener 
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    
        protected override void PerformExecutions(IEventSource session)
        {
            if (log.IsDebugEnabled)
            {
                log.Debug("executing flush");
            }
            try
            {
                session.ConnectionManager.FlushBeginning();
                session.PersistenceContext.Flushing = true;
                session.ActionQueue.PrepareActions();
                session.ActionQueue.ExecuteActions();
            }
            catch (HibernateException exception)
            {
                if (log.IsErrorEnabled)
                {
                    log.Error("Could not synchronize database state with session", exception);
                }
                throw;
            }
            finally
            {
                session.PersistenceContext.Flushing = false;
                session.ConnectionManager.FlushEnding();
            }
    
        }
    } 
    

    Register it during NHibernate configuraiton:

    cfg.EventListeners.FlushEventListeners = new IFlushEventListener[] { new FixedDefaultFlushEventListener() };
    

    You can read more about this bug in Hibernate JIRA: https://hibernate.onjira.com/browse/HHH-2763

    The next release of NHibernate should include that fix either.

    0 讨论(0)
  • 2020-12-31 18:42

    This is not easy at all. I wrote something like this, but it is very specific to our needs and not trivial.

    Some additional hints:

    You can test if references are loaded using

    NHibernateUtil.IsInitialized(entity)
    

    or

    NHibernateUtil.IsPropertyInitialized(entity, propertyName)
    

    You can cast collections to the IPersistentCollection. I implemented an IInterceptor where I get the NHibernate Type of each property, I don't know where you can get this when using events:

    if (nhtype.IsCollectionType)
    {
        var collection = previousValue as NHibernate.Collection.IPersistentCollection;
        if (collection != null)
        {
            // just skip uninitialized collections
            if (!collection.WasInitialized)
            {
                // skip
            }
            else
            {
                // read collections previous values
                previousValue = collection.StoredSnapshot;
            }
        }
    }
    

    When you get the update event from NHibernate, the instance is initialized. You can safely access properties of primitive types. When you want to use ToString, make sure that your ToString implementation doesn't access any referenced entities nor any collections.

    You may use NHibernate meta-data to find out if a type is mapped as an entity or not. This could be useful to navigate in your object model. When you reference another entity, you will get additional update events on this when it changed.

    0 讨论(0)
  • 2020-12-31 18:53

    I was able to solve the same problem using following workaround: set the processed flag to true on all collections in the current persistence context within the listener

    public void OnPostUpdate(PostUpdateEvent postEvent)
    {
        if (IsAuditable(postEvent.Entity))
        {
           //skip application specific code
    
            foreach (var collection in postEvent.Session.PersistenceContext.CollectionEntries.Values)
            {
                var collectionEntry = collection as CollectionEntry;
                collectionEntry.IsProcessed = true;
            }
    
            //var session = postEvent.Session.GetSession(EntityMode.Poco);
            //session.Save(auditTrailEntry);
            //session.Flush();
        }
    }
    

    Hope this helps.

    0 讨论(0)
  • 2020-12-31 19:04

    I was able to determine that this error is thrown when application code loads a Lazy Propery where the Entity has a collection.

    My first attempt involed watching for new CollectionEntries (which I've never want to process as there shouldn't actually be any changes). Then mark them as IsProcessed = true so they wouldn't cause problems.

    var collections = args.Session.PersistenceContext.CollectionEntries;
    var collectionKeys = args.Session.PersistenceContext.CollectionEntries.Keys;
    var roundCollectionKeys = collectionKeys.Cast<object>().ToList();
    var collectionValuesClount = collectionKeys.Count;
    
    // Application code that that loads a Lazy propery where the Entity has a collection
    
    var postCollectionKeys = collectionKeys.Cast<object>().ToList();
    var newLength = postCollectionKeys.Count;
    
    if (newLength != collectionValuesClount) {
    
        foreach (var newKey in postCollectionKeys.Except(roundCollectionKeys)) {
            var collectionEntry = (CollectionEntry)collections[newKey];
            collectionEntry.IsProcessed = true;
        }
    }
    

    However this didn't entirly solve the issue. In some cases I'd still get the exception.

    When OnPostUpdate is called the values in the CollectionEntries dictionary should all already be set to IsProcessed = true. So I decided to do an extra check to see if the collections not processed matched what I expected.

    var valuesNotProcessed = collections.Values.Cast<CollectionEntry>().Where(x => !x.IsProcessed).ToList();
    
    if (valuesNotProcessed.Any()) {
        // Assert: valuesNotProcessed.Count() == (newLength - collectionValuesClount)
    }
    

    In the cases that my first attempt fixed these numbers would match exactly. However in the cases where it didn't work there were extra items alreay in the dictionary. In my I could be sure these extra items also wouldn't result in updates so I could just set IsProcessed = true for all the valuesNotProcessed.

    0 讨论(0)
提交回复
热议问题