I'm working through an issue regarding a couple <subclass>
elements in a "Table-per-class hierarchy" setup in an NHibernate config. I'm trying to fetch the "most-derived-type" of the parent class when I hit the DB. I.e. When I fetch an EnergySource
object, I want the underlying type to be Grid
or Primary
depending on the <discriminator...>
.
Everything actually works as expected if I add the attribute lazy="false"
to the EnergySource
class config. E.g. I can successfully cast with EnergySource as Grid
& I can use reflection on the EnergySource
& if it matches the discriminator, I can run GetType()
& it relays:
UnderlyingSystemType: { Name = "Grid" ...
But with lazy-loading, I instead get a failed cast (only ever null
) &:
UnderlyingSystemType: { Name = "EnergySourceProxy" ...
What's going on here? Is the underlying issue caused by lazy loading in the first place?
I've got my config set up like this (it's an old application):
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Domain.EnergySource, Domain" table="library_EnergySource" lazy="true">
<cache usage="read-write" />
<id name="Id" column="EnergySourceID" unsaved-value="0">
<generator />
</id>
<discriminator formula="case when EnergySourceTypeID in (1,2,3) then 1 else 4 end" />
<property name="Name" />
<many-to-one name="Type" column="EnergySourceTypeID" not-null="true" insert="false" update="false" />
<subclass name="Domain.Grid, Domain"
extends="Domain.EnergySource, Domain"
discriminator-value="1">
</subclass>
<subclass name="Domain.PrimaryEnergy, Domain"
extends="Domain.EnergySource, Domain"
discriminator-value="4">
</subclass>
</class>
</hibernate-mapping>
And the classes are just:
namespace Domain
{
public class Grid : EnergySource { }
public class Primary : EnergySource { }
public class EnergySource
{
public virtual string Name { get; set; }
public virtual EnergySourceType Type { get; set; }
}
public class EnergySourceType
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
}
CodePudding user response:
Essentially, yes, the issue is that the class is "lazy-loaded." When I try and access EnergySource
from another object where it's a property (e.g. item.EnergySource
) I get NHibernate's "proxy", which is there to facilitate lazy-loading, as:
In short... [what we're working with] is a lazy loaded object, NHibernate, at this point in time, has no idea what it is. But since it must return something, it returns a proxy of [our object], which will load the actual instance when needed. [But] that leads to some problems when you want to down cast the value.
See the link for "The problem" for more details. But in essence, once we have a proxied object, we can't just cast it to a subclass of the type it's proxying unfortunately... Here are a few workarounds, taken from the links below:
- Don't let the proxy exist to begin with by disabling lazy-loading using
lazy="false"
- On the class that uses the parent object, add the attribute
lazy="no-proxy"
(in my case, this didn't work, but it may be due to the old version of NHibernate or a mistake in my understanding of the docs; this is certainly what it was designed for):
<many-to-one name="EnergySource" lazy="no-proxy"/>
- Use an NHibernate specific method to "unproxy" the object, creating a new instance of it with the correct type, allowing the cast operation:
return Session.GetSessionImplementation()
.PersistenceContext.Unproxy(EnergySource) as Grid;
- Diegose proposes an interesting workaround, which is to add a property on the parent object that exposes
this
, which works by "leaking a reference to the actual object". This is similar to the above option, but doesn't need anything NH specific to work:
public virtual object Actual { get { return this; } }
...
return item.EnergySource.Actual as Grid //Works
They also provide code for a generic method to simplify this process further. And some caveats, which likely applies to the Unproxy
approach as well.
public virtual T As<T>() where T : Entity
{
return this as T;
}
...
Animal animalProxy = catLover.Pet;
Cat cat = animalProxy.As<Cat>();
// Cat will be the actual object of type Cat
// Or null if animalProxy isn't one
This is a backdoor to the underlying object that NHibernate is managing. It should only be used to access properties of the derived classes. Use polymorphism for behavior. You should never pass the retrieved object to NHibernate methods like Update or Delete.
Sources:
- The problem: https://ayende.com/blog/4168/answer-the-lazy-loaded-inheritance-many-to-one-association-or-m-conundrum
- Similar SO Question: Inheritance and lazy loading in NHibernate
- Diegose
this
workaround: http://sessionfactory.blogspot.com/2010/08/hacking-lazy-loaded-inheritance.html - No Proxy: https://ayende.com/blog/4378/nhibernate-new-feature-no-proxy-associations