16 May 2008

Implicit polymorphism and lazy collections in NHibernate

If you create a lazy loaded property or collection in NHibernate which can contain any type from a class hierarchy, for example by having mappings like this:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="Mammal" table="Mammal">
    <discriminator column="mammal_type" type="String"/>
    ...
    <subclass name="Cat" discriminator-value="CAT">
      ...
      <subclass name="DomesticCat" discriminator-value="CAT-DOMESTIC">
        ...
      </subclass>
    </subclass>
    <subclass name="Dog" discriminator-value="DOG">
      ...
    </subclass>
  </class>
  <class name="Zoo" table="Zoo">
    ...
    <bag name="Animals">
      <key column="zoo_fkey"/>
      <one-to-many class="Mammal"/>
    </bag>
  </class>
</hibernate-mapping>

You'll find that on requesting your objects from your collection they will be of a special new type NHibernate has created, derived from your base class which in this case is "Mammal".

This is a real problem because it means that you can't perform is or as operations on it to determine which actual type it is and you can't cast it to access properties and methods of your derived types.

The solution is to use the Visitor pattern which is described in detail on this site with a couple of examples in C# on this site. Essentially it involves creating a class with a method which is overloaded for each of the types in your class hierarchy.

class MammalVisitor
{
  public void Visit(Cat c) { ... Cat operations ... }
  public void Visit(DomesticCat dc) { ... DomesticCat operations ... }
  public void Visit(Dog d) { ... Dog operations ... }
}

This "visitor" object is then passed to a method defined on the base class of your hierarchy and then subsequently overridden on each derived type.

class Mammal
{
  public virtual void Accept(MammalVisitor mv) { mv.Visit(this); }
  ...
}

class Cat : Mammal
{
  public override void Accept(MammalVisitor mv) { mv.Visit(this); }
  ...  
}

class Dog : Mammal
{
  public override void Accept(MammalVisitor mv) { mv.Visit(this); }
  ...  
}

These methods simply call the visitor's method passing this to it which in turn will automatically execute the correct overload.

Mammal m; // some unknown derived type of mammal
MammalVisitor mv = new MammalVisitor();
m.Accept(mv);

These overloaded methods can then perform your type specific functions. In the example above you have no knowledge of the type of Mammal that you have however when you call Accept the relevant code is automatically executed. If Mammal happens to be a Cat type then the Cat overloaded Visit method is called.

8 comments:

sakumira said...

I don't think so, when the lazy collection is loaded, items will be fetched with correct type according to discriminator value. Unless, a Mammal instance is a proxy (session.Load), but in that case, your visitor will not be able to know about the type of proxy class.

There is just a problem when using polymorphism on lazy property (many-to-one). I am looking for a solution for this case :(

sakumira said...

I don't think your visitor can work if Mammal is a proxy.

For polymorphic one-to-many lazy property, it works as well in NHibernate. There is just a problem on polymorphic many-to-one lazy property (proxy). I am looking for a solution on this case too.

Derek Fowler said...

The items of the collection are proxies, that is the nature of the problem. The proxy is a subclass of the class specified as the collection item, in this case Mammal. When the object is hydrated from the database you end up with the correct type however it is still "boxed" as a Mammal so you can't test the type to see what it is you have.

Visitor works because the object itself knows what type it is so when you pass in your visitor to the object the object calls the correct overloaded method on your visitor.

The solution is the same for a lazy one-to-many property.

sakumira said...

I have checked, the call to a method (other than GetType, GetHashCode methods) of a proxy will trigger lazy-load the object fully, and call the method of real object. Therefore, thank you for your solution.

Now, I am facing another big problem with polymorphism in NHibernate. The polymorphic query does not work with any ResultTransformer when Query Cache is enable :( Do you have any idea?

Derek Fowler said...

I think this is down to issue NH-1090 but http://jira.nhibernate.org/ is timing out at the moment so I can't check.

If I remember rightly the transform is being applied before the caching occurs so NHibernate expects something like an object[] but ends up with something else.

I don't think there is a work around for this - we ended up having to hack the source of NHibernate. I can't remember whether it was straightforward or not.

sakumira said...

Me too, I cannot see the source code of NH-1090 ;( No, the transform is being applied after the caching occurs. That's why I wonder What's the real problem?

You could see the code of Loader.ListUsingQueryCache method, and StandardQueryCache.Put method.

Anonymous said...

I think the easiest way is to add a method to the base class:

public virtual T GetAsType<T>() where T :class
{
return this as T;
}

Derek Fowler said...

I'm not sure that will work as you're still trying to cast the object to a type it isn't derived from. Also even if you did use this method you'd end up needing lots of calls to GetAsType<>() and if(){} statements to check the cast was successful, the resulting code wouldn't be as elegant.