Using the Ghostbusters test

As a part of automatic dirty checking, NHibernate compares the original state of an entity to its current state. An otherwise unchanged entity may be updated unnecessarily because a type conversion caused this comparison to fail. In this recipe, we will show you how to detect these "ghost update" issues with the Ghostbusters test.

Getting ready

Complete the Fast testing with the SQLite in-memory database recipe in this chapter.

How to do it...

  1. Add a new class named Ghostbusters using the following code:
    private static readonly ILog log =
      LogManager.GetLogger(typeof(Ghostbusters));
    
    private readonly Configuration _configuration;
    private readonly ISessionFactory _sessionFactory;
    private readonly Action<string> _failCallback;
    private readonly Action<string> _inconclusiveCallback;
    
    public Ghostbusters(Configuration configuration,
    ISessionFactory sessionFactory,
    Action<string> failCallback,
    Action<string> inconclusiveCallback)
    {
      _configuration = configuration;
      _sessionFactory = sessionFactory;
      _failCallback = failCallback;
      _inconclusiveCallback = inconclusiveCallback;
    }
    
    public void Test()
    {
      var mappedEntityNames = _configuration.ClassMappings
      .Select(mapping => mapping.EntityName);
    
      foreach (string entityName in mappedEntityNames)
        Test(entityName);
    }
    
    public void Test<TEntity>()
    {
      Test(typeof(TEntity).FullName);
    }
    
    public void Test(string entityName)
    {
      object id = FindEntityId(entityName);
      if (id == null)
      {
        var msg = string.Format(
         "No instances of {0} in database.", 
          entityName);
        _inconclusiveCallback.Invoke(msg);
        return;
      }
      log.DebugFormat("Testing entity {0} with id {1}", 
        entityName, id);
      Test(entityName, id);
    }
    
    public void Test(string entityName, object id)
    {
      var ghosts = new List<String>();
      var interceptor = new GhostInterceptor(ghosts);
    
      using (var session = _sessionFactory.OpenSession(interceptor))
      using (var tx = session.BeginTransaction())
      {
        session.Get(entityName, id);
        session.Flush();
        tx.Rollback();
      }
    
      if (ghosts.Any())
        _failCallback.Invoke(string.Join("
    ", ghosts.ToArray()));
    }
    
    private object FindEntityId(string entityName)
    {
      object id;
      using (var session = _sessionFactory.OpenSession())
      {
        var idQueryString = string.Format(
          "SELECT e.id FROM {0} e", 
          entityName);
    
        var idQuery = session.CreateQuery(idQueryString)
        .SetMaxResults(1);
    
        using (var tx = session.BeginTransaction())
        {
          id = idQuery.UniqueResult();
          tx.Commit();
        }
      }
      return id;
    }
  2. Add another class named GhostInterceptor using the following code:
    private static readonly ILog log = 
      LogManager.GetLogger(typeof(GhostInterceptor));
    
    private readonly IList<string> _ghosts;
    private ISession _session;
    
    public GhostInterceptor(IList<string> ghosts)
    {
      _ghosts = ghosts;
    }
    
    public override void SetSession(ISession session)
    {
      _session = session;
    }
    
    public override bool OnFlushDirty(
    object entity, object id, object[] currentState,
    object[] previousState, string[] propertyNames, IType[] types)
    {
      var msg = string.Format("Flush Dirty {0}", 
        entity.GetType().FullName);
      log.Error(msg);
      _ghosts.Add(msg);
      ListDirtyProperties(entity);
      return false;
    }
    
    public override bool OnSave(
    object entity, object id, object[] state,
    string[] propertyNames, IType[] types)
    {
      var msg = string.Format("Save {0}", 
        entity.GetType().FullName);
      log.Error(msg);
      _ghosts.Add(msg);
      return false;
    }
    
    public override void OnDelete(
    object entity, object id, object[] state,
    string[] propertyNames, IType[] types)
    {
      var msg = string.Format("Delete {0}", 
        entity.GetType().FullName);
      log.Error(msg);
      _ghosts.Add(msg);
    }
    
    private void ListDirtyProperties(object entity)
    {
      string className = 
        NHibernateProxyHelper.GuessClass(entity).FullName;
    
      var sessionImpl = _session.GetSessionImplementation();
    
      var persister = 
        sessionImpl.Factory.GetEntityPersister(className);
    
      var oldEntry = 
        sessionImpl.PersistenceContext.GetEntry(entity);
    
      if ((oldEntry == null) && (entity is INHibernateProxy))
      {
        var proxy = entity as INHibernateProxy;
        object obj = 
          sessionImpl.PersistenceContext.Unproxy(proxy);
    
        oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);
      }
    
      object[] oldState = oldEntry.LoadedState;
    
      object[] currentState = persister.GetPropertyValues(entity, 
        sessionImpl.EntityMode);
    
      int[] dirtyProperties = persister.FindDirty(currentState, 
        oldState, entity, sessionImpl);
    
      foreach (int index in dirtyProperties)
      {
        var msg = string.Format(
          "Dirty property {0}.{1} was {2}, is {3}.",
          className,
          persister.PropertyNames[index],
          oldState[index] ?? "null",
          currentState[index] ?? "null");
        log.Error(msg);
        _ghosts.Add(msg);
      }
    
    }
  3. Add the following test to the PersistenceTests fixture:
    [Test]
    public void GhostbustersTest()
    {
    
      using (var tx = Session.BeginTransaction())
      {
    
        Session.Save(new Movie()
        {
          Name = "Ghostbusters",
          Description = "Science Fiction Comedy",
          Director = "Ivan Reitman",
          UnitPrice = 7.97M,
          Actors = new List<ActorRole>()
          {
            new ActorRole() 
            { 
              Actor = "Bill Murray",
              Role = "Dr. Peter Venkman"
            }
          }
        });
    
        Session.Save(new Book()
        {
          Name = "Who You Gonna Call?",
          Description = "The Real Ghostbusters comic series",
          UnitPrice = 30.00M,
          Author = "Dan Abnett",
          ISBN = "1-84576-141-3"
        });
    
        tx.Commit();
      }
    
      new Ghostbusters(
        NHibernateSessionFactoryProvider.Configuration,
        NHibernateSessionFactoryProvider.SessionFactory,
        new Action<string>(msg => Assert.Fail(msg)),
        new Action<string>(msg => Assert.Inconclusive(msg))
      ).Test();
    
    
    }
  4. Run the tests with NUnit.

How it works...

The Ghostbusters test finds issues where a session's automatic dirty checking determines that an entity is dirty (has unsaved changes) when, in fact, no changes were made. This can happen for a few reasons, but it commonly occurs when a database field that allows nulls is mapped to a non-nullable property, such as integer or DateTime, or when an enum property is mapped with type="int". For example, when a null value is loaded into an integer property, the value is automatically converted to the integer's default value, zero. When the session is flushed, automatic dirty checking will see that the value is no longer null and update the database value to zero. This is referred to as a "ghost" update.

At the heart of our Ghostbusters test, we have the GhostInterceptor. An interceptor allows an application to intercept session events before any database action occurs. This interceptor can be set globally on the NHibernate configuration or passed as a parameter to sessionFactory.OpenSession, as we've done in this recipe.

When we flush a session containing a dirty entity, the interceptor's OnFlushDirty method is called. GhostInterceptor compares the current values of the dirty entity's properties to their original values and reports these back to our Ghostbusters class. Similarly, we also intercept Save and Delete events, though these are much less common.

Our Ghostbusters class coordinates the testing. For example, we can call Test(entityName,id) to test using a particular instance of an entity. If we strip this test down to its core, we end up with the following code:

session.Get(entityName, id);
session.Flush();
tx.Rollback();

Note that we simply get an entity from the database and immediately flush the session. This runs automatic dirty checking on a single unchanged entity. Any database changes resulting from this Flush() are ghosts.

If we call Test(entityName) or Test<Entity>(), Ghostbusters will first query the database for an ID for the entity, then run the test. For a test on our Movie entity, this ID query would be:

SELECT e.id FROM Eg.Core.Movie e

This lowercase id property has special meaning in HQL. In HQL, lowercase id always refers to the entity's POID. In our model, it happens to be named Id, but we could have just as easily named it "Bob."

Finally, if we simply call the Test() method, Ghostbusters will test one instance of each mapped entity. We used this method in our tests.

This Ghostbusters test has somewhat limited value in the automated tests as we've done here. It really shines when testing migrated or updated production data.

See also

  • Using the Hibernate Query Language
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.188.121