Or "a tale of a day spent debugging".
We've advanced quite far along in quite a big application using NHibernate. I keep an eye on the statements NHibernate generates, making sure that nothing too untoward is happening. I was inspecting one test (not so great to begin with, one that got everything from a database, just to retrieve the first one), and I noticed that every single item being loaded was being updated on flush.
Hmmmm. Doesn't seem right, my honed QA skills told me. Let's take a look. Doesn't look like anything's set. Let's put some breakpoints in the constructors, and take it from there. Hmmm, this is a mighty big object, loads of children, must be one of them setting something on construction. Strange, nothing there. And so my day continued.
To illustrate, let me introduce you to my friend, Enummy. He's quite simple, only really has an id and an emotion.
public class Enummy
{
public virtual int Id { get; set; }
public virtual Emotions Emotion { get; set; }
}
public enum Emotions
{
Happy,
Sad,
Frustated,
Annoyed
}
Naturally we should test that he has the range of emotions, so we had whapped out the following tests.
[Test]
public void Enummy_should_be_happy_before_and_after_save()
{
int id;
ISession session = _factory.OpenSession();
var enummy = new Enummy {Emotion = Emotions.Happy};
session.Save(enummy);
session.Flush();
id = enummy.Id;
session = _factory.OpenSession();
var loaded = session.Get<Enummy>(id);
Assert.That(loaded.Emotion, Is.EqualTo(Emotions.Happy));
}
[Test]
public void Enummy_should_sometimes_be_sad()
{
int id;
ISession session = _factory.OpenSession();
var enummy = new Enummy {Emotion = Emotions.Sad};
session.Save(enummy);
session.Flush();
id = enummy.Id;
session = _factory.OpenSession();
var loaded = session.Get<Enummy>(id);
Assert.That(loaded.Emotion, Is.EqualTo(Emotions.Sad));
}
So far so good. That's the complete round trip, right there, all the way to the database. A proper integration test against SQL Server. We mapped Enummy with the following mapping:
<hibernate-mapping default-cascade="none" xmlns="urn:nhibernate-mapping-2.2">
<class name="Domain.Enummy, Domain" table="Enummy">
<id name="Id" type="System.Int32" column="Id" unsaved-value="0">
<generator class="hilo" />
</id>
<property name="Emotion" type="System.Int32" />
</class>
</hibernate-mapping>
Test pass, so all seems good. Except, of course, for the dirty session (one test you're not likely to write).
[Test]
public void Enummy_should_not_dirty_the_session_when_he_loads()
{
int id;
ISession session = _factory.OpenSession();
var enummy = new Enummy { Emotion = Emotions.Frustated };
session.Save(enummy);
session.Flush();
id = enummy.Id;
session = _factory.OpenSession();
var loaded = session.Get<Enummy>(id);
Assert.IsFalse(session.IsDirty());
}
Enummy, little guy, I realise you're frustrated, but must you dirty yourself and the session so? You even do it when you're happy! Assert.That(debugger.Emotion, Is.EqualTo(Emotions.Annoyed)) :(
The problem is with that mapping. An easy mistake to make, especially if you don't realise that NHibernate has no issues with .NET enums, and presumed that an integer (or even smaller) would be appropriate. Enums should be mapped like so, with the full type of the enum specified.
<hibernate-mapping default-cascade="none" xmlns="urn:nhibernate-mapping-2.2">
<class name="Domain.Enummy, Domain" table="Enummy">
<id name="Id" type="System.Int32" column="Id" unsaved-value="0">
<generator class="hilo" />
</id>
<property name="Emotion" type="Domain.Emotions, Domain" />
</class>
</hibernate-mapping>
The cast from Int32 to an enum (albeit implicit at times) is enough to dirty the object. To reiterate (in the interests of good SEO), an NHibernate entity and session will be marked dirty even if nothing is explicitly set if there is a cast from one type to another in the mapping. The values might be equal when one is cast to the type of the other. Until one is cast, though, they are unequal, and as such the object is considered to be dirty. While really hard to debug, I suppose this is the correct behaviour. And once you know about it, debugger.Emotion = Emotions.Happy.
Another item on the code review checklist, watch for enums and other casts in NHibernate mappings, and add an integration test that the object is not dirty straight after a load (for that next newbie to NH who doesn't realise this).
Update: After having struggled through this all on my own, I was relieved to find that I wasn't the only one. I wish I had stumbled across metro-dev extraordinaire Justice Gray's post explaining the issue before. By his reckoning I'm in the 0.0000000000000001%. I like to think I'm even more special.
Technorati Tags:
NHibernate