Try fast search NHibernate

17 October 2008

How Test your mappings: the Ghostbuster

In NHibernate, when you have the FulshMode configured to AutoFlush, a session.Flush() is thrown when NH detect a dirty entity instance and you are querying an intersected QuerySpace (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>

If you have the above domain, and you are going to query the Reptile class in a opened session with a dirty instance of Animal, a session.Flush() will be thrown.

After a session.Get<Animal>(anId) 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 depend.

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 don’t break 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.


Technorati Tags: ,,

7 comments:

  1. Hi Fabio, I think there is an error in the class.

    In this line:
    from persistentClass in cfg.ClassMappings
    where entitiesFilter.Contains(persistentClass.EntityName)
    select persistentClass.EntityName;

    EntitiesFilter has a "Person" item, but the EntityName in the cfg.classMappings include the namespace "Ghostbusters.Person".

    "Person".contains("Ghostbusters.Person") is false an the result collection is empty.

    ReplyDelete
  2. I've fixed in this way (BTW I'm very new to linq, probabily not the best solution).

    entitiesToCheck = from persistentClass in cfg.ClassMappings
    from EntitieToFilter in (from EntitieToFilter in entitiesFilter
    where persistentClass.EntityName.Contains(EntitieToFilter)
    select EntitieToFilter).DefaultIfEmpty()
    where EntitieToFilter != null
    select persistentClass.EntityName;

    ReplyDelete
  3. So...umm... how do you remove the ghost in this instance? Is it possible?

    ReplyDelete
  4. Each situation has a different solution. Some one is easy some other is not so easy to fix (not all "ghost" are so easy to remove)... the difference is that now you know you have a "ghost" and you know where is it.

    ReplyDelete
  5. Excellent information... I'll try it for a problem that we have here... Thanks!

    ReplyDelete
  6. Hola Fabio, estaba probando esto pero me esta dando el siguiente error :

    Unable to load type 'NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle' during configuration of proxy factory class.
    Possible causes are:
    - The NHibernate.Bytecode provider assembly was not deployed.
    - The typeName used to initialize the 'proxyfactory.factory_class' property of the session-factory section is not well formed.

    Solution:
    Confirm that your deployment folder contains one of the following assemblies:
    NHibernate.ByteCode.LinFu.dll
    NHibernate.ByteCode.Castle.dll

    ReplyDelete