Logo

NHibernate

The object-relational mapper for .NET

How Test your mappings: the Ghostbuster

In NHibernate, when you have the FlushMode configured to AutoFlush, session.Flush() is called when NH detects a dirty entity instance and when a query with an intersected QuerySpace is performed. (The QuerySpace is represented by all tables affected in a query.)

Example:

  <class name="Animal">
<
id name="Id">
<
generator class="hilo"/>
</
id>
<
property name="Description"/>

<
joined-subclass name="Reptile">
<
key column="animalId"/>
<
property name="BodyTemperature"/>
</
joined-subclass>

</
class>

In the above domain, a query on the Reptile class in a opened session with a dirty instance of Animal would cause session.Flush() will be called.

After a session.Get<Animal>(animalId) we can be pretty sure that there is no dirty entities in the session, sure ?

Don’t be so sure! The real answer is: It depends.

For example try this domain:

public enum Sex
{
Unspecified,
Male,
Female
}
public class Person
{
public virtual int Id { get; set; }
public virtual Sex Sex { get; set; }
}

with this mapping:

  <class name="Person">
<
id name="Id">
<
generator class="hilo"/>
</
id>
<
property name="Sex" type="int"/>
</
class>

In the mapping I define the property Sex of type int but in the class the type is Sex; even if you don’t receive an exception, because an int is convertible to Sex and viceversa, your persistence will have a unexpected  behavior. NH will detect a modification, of your entity, “immediately” after session.Get because it having an int in the entity snap-shot (retrieved from DB) and a Sex in the actual state. The example are showing a very simple case of “ghosts” in your application. In a big environment, with a complex domain, find “ghosts” it is not so easy.

The Ghostbusters

[TestFixtureSetUp]
public void TestFixtureSetUp()
{
XmlConfigurator.Configure();
cfg = new Configuration();
cfg.Configure();
new SchemaExport(cfg).Create(false, true);
sessions = (ISessionFactoryImplementor) cfg.BuildSessionFactory();
PopulateDb();
}

Few words about the TestFixtureSetUp:


  • if you are testing your domain persistence you can run the “ghostbuster” in each test.
  • if you are testing yours DAOs and you have an implementation of ObjectMother or TestDataBuilder you can use it in the implementation of PopulateDb() method.
  • If you don’t have tests you can leave the PopulateDb() method empty and configure NH to an existing copy of your DB.
[Test, Explicit]
public void UnexpectedUpdateDeleteOnFetch()
{
PersistingMappings(null);
}

[Test, Explicit]
public void UnexpectedUpdateDeleteOnFetchSpecific()
{
var entitiesFilter = new[]
{
"Person"
};
PersistingMappings(entitiesFilter);
}

In my experience the above two tests are needed. The first sound like “close your eyes and pray” the second allow you to analyze some specific entities.

To avoid breaking the test, on each unexpected DB-hit, I’ll use the power of log4net in the whole fixture.

To intercept unexpected Flush a possible, easy and quickly, way is an implementation of IInterceptor.

private class NoUpdateInterceptor : EmptyInterceptor
{
private readonly IList<string> invalidUpdates;

public NoUpdateInterceptor(IList<string> invalidUpdates)
{
this.invalidUpdates = invalidUpdates;
}

public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types)
{
string msg = " FlushDirty :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
return false;
}

public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
string msg = " Save :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
return false;
}

public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
string msg = " Delete :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
}
}

As you can see I’m interested in : unexpected Flush of dirty instance, unexpected Saves and unexpected Deletes.

The PersistingMappings is my “driver” to test each entity. The responsibility of the method is iterate each persistent class known by the SessionFactory (or the selected in UnexpectedUpdateDeleteOnFetchSpecific methods), run the test of each entity and reports all issues found.

private void PersistingMappings(ICollection<string> entitiesFilter)
{
var invalidUpdates = new List<string>();
var nop = new NoUpdateInterceptor(invalidUpdates);

IEnumerable<string> entitiesToCheck;
if (entitiesFilter == null)
{
entitiesToCheck = cfg.ClassMappings.Select(x => x.EntityName);
}
else
{
entitiesToCheck = from persistentClass in cfg.ClassMappings
where entitiesFilter.Contains(persistentClass.EntityName)
select persistentClass.EntityName;
}

foreach (var entityName in entitiesToCheck)
{
EntityPersistenceTest(invalidUpdates, entityName, nop);
}

if (invalidUpdates.Count > 0)
{
if (logError.IsDebugEnabled)
{
logError.Debug(" ");
logError.Debug("------ INVALID UPDATES -------");
invalidUpdates.ForEach(x => logError.Debug(x));
logError.Debug("------------------------------");
}
}
Assert.AreEqual(0, invalidUpdates.Count, "Has unexpected updates.");
}

To check each persistent entity I’m using the Configuration.ClassMappings collection and extracting the EntityName from the PersistentClass. The use of EntityName don’t mean that I’m using the tag entity-name (as you can see in the mapping above).

The real “ghostbuster” is:

private void EntityPersistenceTest(ICollection<string> invalidUpdates,
string entityName, IInterceptor nop)
{
const string queryTemplate = "select e.{0} from {1} e";
string msg = "s--------" + entityName;
log.Debug(msg);

using (var s = sessions.OpenSession(nop))
using (var tx = s.BeginTransaction())
{
IList entityIds = null;
try
{
string queryString = string.Format(queryTemplate, DefaultIdName, entityName);
entityIds = s.CreateQuery(queryString).SetMaxResults(1).List();
}
catch (Exception e)
{
log.Debug("Possible METEORITE:" + e.Message);
}

if (entityIds != null)
{
if (entityIds.Count == 0 || entityIds[0] == null)
{
log.Debug("No instances");
}
else
{
if (entityIds.Count > 1)
{
msg = ">Has " + entityIds.Count + " subclasses";
log.Debug(msg);
}
object entityId = entityIds[0];
try
{
s.Get(entityName, entityId);
try
{
s.Flush();
}
catch (Exception ex)
{
string emsg = string.Format("EXCEPTION - Flushing entity [#{0}]: {1}", entityId, ex.Message);
log.Debug(emsg);
invalidUpdates.Add(emsg);
}
}
catch (Exception ex)
{
string emsg = string.Format("EXCEPTION - Getting [#{0}]: {1}", entityId, ex.Message);
invalidUpdates.Add(emsg);
log.Debug(emsg);
}
}
tx.Rollback();
}
}
msg = "e--------" + entityName;
log.Debug(msg);
}

The core of the test is:

s.Get(entityName, entityId);
s.Flush();

If I Get an entity, from a clear fresh session, without touch the state what I’m expect is that the follow Flush don’t  make absolutely nothing but… you know… perhaps there is an ugly “ghost”. Each try-catch are checking some special situation.

And now lets go to run the “ghostbuster” in your application. Code available here.


Posted Mon, 20 October 2008 03:48:00 AM by fabiomaulo
Filed under: mapping, NHibernate, Tests

comments powered by Disqus
© NHibernate Community 2024