EDIT:
It turned out that the original example herein is not very clear about the point I want to make. Therefore I posted a follow-up with a better and more precise example (see
here).
Recently, I came about this blog post, which describes an interesting problem about NHibernate, lazy loading, and polymorphism. My initial thoughts were that this would be another one of these fancy technical details that a developer has to deal with all the time. But after a while it grew bigger and bigger in my mind and now I consider it to be a 'real' problem that has the potential to question our entire understanding of architectural layering, if you're not aware of it.
This is the first one of a two-part post. In this part I will describe the problem in some detail and I will also explain why I think it's a big one. The second part (which I will post within the next few days) will then present an architectural approach to avoid the here outlined problems.
The scenario
As an example, I will use a classic many-to-one relationship like it regularly occurs in every project. (It's important to understand that the following is far from being somehow exotic or exceptional.) Here's the - intentionally simple - 'domain model':

And this is the corresponding 'data model':

Nothing unusual here also - the data model is even simpler than the class model, because the entire Author inheritance tree is mapped to a single table. A Book has an Author, which is accomplished by the 'BOOKS.AUTHOR->AUTHORS.ID' foreign key relationship. The actual type of the author is determined by the AUTHORS.TYPE column, which is one of 'P' or 'E'. (Note that something like an AuthorType property is not present in the domain in any way, the concept of "An author must be either a person or a group of editors" is expressed by the author's concrete class type. The TYPE database column serves only as discriminator value for persistence purposes.)
With NHibernate, the mapping between classes and database tables would be:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LLDemo.Core" namespace="LLDemo.Core">
<class name="Author" table="AUTHORS" abstract="true">
<id name="Id">
<generator class="native" />
</id>
<discriminator column="TYPE"/>
<property name="Line" />
<subclass name="Person" discriminator-value="P"/>
<subclass name="Editors" discriminator-value="E"/>
</class>
</hibernate-mapping>
...
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="LLDemo.Core" namespace="LLDemo.Core">
<class name="Book" table="BOOKS">
<id name="Id">
<generator class="native" />
</id>
<property name="Name" />
<many-to-one name="Author"/>
</class>
</hibernate-mapping>
Quite trivial stuff so far, and definitely not worth a blog post. But wait, there's more...
Equipped with the above depicted knowledge about the 'domain model', it seems absolutely reasonable and natural to write code like this:
Book book = GetABook();
if (book.Author is Person)
{
// do something person-specific
}
else if (book.Author is Editors)
{
// do something editors-specific
}
|
And this is where the trouble starts. Because NHibernate uses lazy loading by default, the Author property will never be of any type defined in our code, but it will always be a dynamic proxy created by the ORM at runtime - this remains true even if the Author finally gets loaded: The proxy won't be replaced by the 'real' entity (this is simply not possible in .NET), instead the calls will be forwarded to the now loaded Person entity. So the above code will never work - which is btw. not something NH-specific, but will appear in every ORM by design, if it supports lazy loading and inheritance.
To see the problem in some more detail, look at this test:
[Test]
public void TheAuthorOfFightClubShouldBeAPerson()
{
using (ISession session = this.sessionFactory.OpenSession())
{
Book fightClub = session.Get<Book>(1);
Console.WriteLine("This is just to see lazy loading happening...");
Assert.IsNotNull(fightClub.Author);
Assert.AreEqual("Chuck Palahniuk", fightClub.Author.Line);
// this fails when lazy loading is used...
Assert.IsInstanceOfType(typeof(Person), fightClub.Author);
}
}
|
This gives the following result - the console output together with the failure message illustrates quite clear what's going on:
Think about that for a minute. What does it mean in the end?
It means that the entire concept of Persistence ignorance is at risk here! The whole point of Persistence ignorance is that the domain should be unaware of how, or even if, it is being persisted. Or, in other words: "There is no database". But having the here described scenario, it is far too easy to introduce errors like the above. To avoid such things, you have to know a) that an ORM is used to handle persistence, b) that the ORM is configured to use lazy loading, and c) you need some knowledge about how an ORM works. This is one of the cases where seemingly simple classes can only be handled correctly if the developer has the complexity of the underlying persistence system in mind, although there is no obvious dependency between the Author class and the way it is persisted. Everything seems to be pure POCO/Persistence ignorance...
While the above outlined error source might not be a big deal for the initial developer, it can become a huge, annoying problem for the poor guy who has to deal with this code in the future. Imagine yourself starting a new job and inheriting a large codebase that seems to be fairly well-structured and good to maintain. Let the codebase be 10-15 years of age (which is far from uncommon in real life). You have no detailed knowledge about ORMs - they might be out of fashion at the time and anyway the domain doesn't depend on the persistence layer in any respect, right? Now you have to do an extension, and in the course of this you introduce code like the above, without any suspect. I promise you: You will have a nice time finding and understanding the problem (and your company will have to spend a lot of money for it)...
I will end this here (okay, somewhat abruptly, but it's already too long, I think...), leaving the 'solution' for the second part of this post...