Getting proxies of the correct type in NHibernate

2019-01-23 12:51发布

问题:

I have a problem with uninitialized proxies in nhibernate

The Domain Model

Let's say I have two parallel class hierarchies: Animal, Dog, Cat and AnimalOwner, DogOwner, CatOwner where Dog and Cat both inherit from Animal and DogOwner and CatOwner both inherit from AnimalOwner. AnimalOwner has a reference of type Animal called OwnedAnimal.

Here are the classes in the example:

public abstract class Animal
{
   // some properties
}

public class Dog : Animal
{
   // some more properties
}

public class Cat : Animal
{
   // some more properties
}

public class AnimalOwner 
{
   public virtual Animal OwnedAnimal {get;set;}
   // more properties...
}

public class DogOwner : AnimalOwner
{
   // even more properties
}

public class CatOwner : AnimalOwner
{
   // even more properties
}

The classes have proper nhibernate mapping, all properties are persistent and everything that can be lazy loaded is lazy loaded.

The application business logic only let you to set a Dog in a DogOwner and a Cat in a CatOwner.

The Problem

I have code like this:

public void ProcessDogOwner(DogOwner owner)
{
   Dog dog = (Dog)owner.OwnedAnimal;
   ....
}

This method can be called by many diffrent methods, in most cases the dog is already in memory and everything is ok, but rarely the dog isn't already in memory - in this case I get an nhibernate "uninitialized proxy" but the cast throws an exception because nhibernate genrates a proxy for Animal and not for Dog.

I understand that this is how nhibernate works, but I need to know the type without loading the object - or, more correctly I need the uninitialized proxy to be a proxy of Cat or Dog and not a proxy of Animal.

Constraints

  • I can't change the domain model, the model is handed to me by another department, I tried to get them to change the model and failed.
  • The actual model is much more complicated then the example and the classes have many references between them, using eager loading or adding joins to the queries is out of the question for performance reasons.
  • I have full control of the source code, the hbm mapping and the database schema and I can change them any way I want (as long as I don't change the relationships between the model classes).
  • I have many methods like the one in the example and I don't want to modify all of them.

Thanks,
Nir

回答1:

It's easiest to turn off lazy loading for the animal class. You say it's mostly in memory anyway.

<class name="Animal" lazy="false">
<!-- ... -->
</class>

As a variant of that, you could also use no-proxy, see this post:

<property name="OwnedAnimal" lazy="no-proxy"/>

As far as I can see, it only works when the AnimalOwner actually is a proxy.

OR

You can use generics on the animal owner to make the reference a concrete class.

class AnimalOwner<TAnimal>
{
  virtual TAnimal OwnedAnimal {get;set;}
}

class CatOwner : AnimalOwner<Cat>
{
}

class DogOwner : AnimalOwner<Dog>
{
}

OR

You can map the DogOwners and CatOwners in separate tables, and define the concrete animal type in the mapping.

<class name="CatOwner">
  <!-- ... -->
  <property name="OwnedAninal" class="Cat"/>
</class>
<class name="DogOwner">
  <!-- ... -->
  <property name="OwnedAninal" class="Dog"/>
</class>

OR

You mess a little around with NHibernate, as proposed in this blog. NH is actually able to return the real object behind the proxy. Here a bit simpler implementation as proposed there:

    public static T CastEntity<T>(this object entity) where T: class
    {
        var proxy = entity as INHibernateProxy;
        if (proxy != null)
        {
            return proxy.HibernateLazyInitializer.GetImplementation() as T;
        }
        else
        {
            return entity as T;
        }
    }

which can be used like this:

Dog dog = dogOwner.OwnedAnimal.CastEntit<Dog>();


回答2:

I think we recently had a similar problem, AFAIR solution was to give 'Animal' a self -"method/property":

public Animal Self { get { return this; } }

This could then be casted to correct "animal". What happens is that your original object has a reference to nhibernate proxy object (when it's lazily loaded), which acts as Animal for all methods exposed via Animal class (it passes all calls to the loaded object). However it cannot be casted as any of your other animals because it is none of these, it only emulates the Animal class. However the class that is encapsulated by AnimalProxy can be casted as subclassed animal because it is a real instance of correct class, you only need to get to it's this reference.



回答3:

You might want to try this to see the proxied type (assuming NH 2.0+):

((INHibernateProxy)proxy).HibernateLazyInitializer.PersistentClass

But this kind of casting or "type peeking" is very bad practice anyway...



回答4:

If we've been working with the same problem the issue is that the proxy generated is the proxy of Animal rather than of Dog.

The solution we used was to reload the object:

Dog dog = this.CurrentSession.Load<Dog>(owner.OwnedAnimal.AnimalID);

This goes back to your session and reloads the object with the correct type.

Hope this helps



回答5:

If you use Fluent NHibernate, you can use an auto-mapping override to turn off lazy loading for that property only:

public class DogOwnerMapOverride : IAutoMappingOverride<DogOwner>
{
    public void Override( AutoMapping<DogOwner> mapping )
    {
        mapping.References( x => x.OwnedAnimal ).Not.LazyLoad();
    }
}


回答6:

You can try put this method on your base entity:

public virtual T As<T>() where T : Entity {
      return this as T;
}