开发者

Equals implementation of NHibernate Entities, unproxy question

开发者 https://www.devze.com 2023-02-25 18:56 出处:网络
In NHibernate 3.0 Cookbo开发者_JAVA技巧ok, there is a sample implementation for a base Entity type. The equals is implemented like this:

In NHibernate 3.0 Cookbo开发者_JAVA技巧ok, there is a sample implementation for a base Entity type. The equals is implemented like this:

public abstract class Entity<TId>
{
  public virtual TId Id { get; protected set; }

  public override bool Equals(object obj)
  {
    return Equals(obj as Entity<TId>);
  }

  private static bool IsTransient(Entity<TId> obj)
  {
     return obj != null && Equals(obj.Id, default(TId));
  }  

  private Type GetUnproxiedType()
  {
     return GetType();
  }  

  public virtual bool Equals(Entity<TId> other)
  {
    if (other == null) return false;            
    if (ReferenceEquals(this, other)) return true;

    if (!IsTransient(this) && !IsTransient(this) && Equals(Id, other.Id))
    {
      var otherType = other.GetUnproxiedType();
      var thisType = GetUnproxiedType();
      return thisType.IsAssignableFrom(otherType) ||
         otherType.IsAssignableFrom(thisType);
    }
    return false;
  }    
}

The reason for the GetUnproxiedType() method is this: There is an abstract base class Product, a concrete class Book which inherits from Product and a dynamic proxy class ProductProxy used by NHibernate for lazy loading. If a ProductProxy representing a Book and a concrete Book have the same Ids, they should be treated as equal. However I don't really see why calling GetType() on a ProductProxy instance should return Product in this case, and how it helps. Any ideas?


I actually went ahead and wrote to the author of the book about this code. It turns out this is due to how the proxy wrapping works. Here is his response:

"If you don't understand how the proxy frameworks work, the idea can seem magical.

When NHibernate returns a proxy for the purposes of lazy loading, it returns a proxy instance inherited from the actual type. There are a few members we can access without forcing a load from the database. Among these are proxy's Id property or field, GetType(), and in some circumstances Equals() and GetHashCode(). Accessing any other member will force a load from the database.

When that happens, the proxy creates an internal instance. So, for example, a lazy loaded instance of Customer (CustomerProxy102987098721340978), when loaded, will internally create a new Customer instance with all of the data from the database. The proxy then does something like this:

public overrides string Name 
{ 
    get { 
       return _loadedInstance.Name; 
    } 
    set { _loadedInstance.Name = value; } 
}

Incidentally, it's this overriding that requires everything to be virtual on entities that allow lazy loaded.

So, all calls to the Name property on the proxy are relayed to the internal Customer instance that has the actual data.

GetUnproxiedType() takes advantage of this. A simple call to GetType() on the proxy will return typeof(CustomerProxy02139487509812340). A call to GetUnproxiedType() will be relayed to the internal customer instance, and the internal customer instance will return typeof(Customer)."


With current (v5.x) NHibernate proxy factories (static or dynamic, static being available since v5.1), this pattern is actually broken. The v5 built-in proxy factories do not intercept private methods.

And I think that was already the case for v4 ones.

For this pattern to work with current built-in proxy factories, GetUnproxiedType should be virtual (so not private by the way, but protected).

Otherwise, use NHibernateUtil.GetClass, which is meant for this and does not rely on brittle tricks. Its documentation warns it will initialize the proxy by side effect, but anyway the GetUnproxiedType trick must do the same for working.
Of course using NHibernateUtil.GetClass means having a direct dependency to NHibernate in a domain model base class. But depending on an implementation trick specific to an external (from the domain viewpoint) library implementation is no better in my opinion.

Moreover, some changes may cause the GetUnproxiedType trick to be even more broken in the future, like some ideas for reducing the number of cases causing a proxy to get initialized when it could be avoided. (See here by example.)

If you really want a GetUnproxiedType method not depending on a direct NHibernate reference, I think the only theoretically "safe" solution is to have it abstract and overridden in each concrete entity class for yielding typeof(YourEntityClass). But in practice, it would be cumbersome and error-prone (bad copy-paste for creating a new entity, forgetting to change that method...), while the abstract part won't help in case some concrete entity classes are further specialized through inheritance.

Another trick could be, from the type obtained by GetType, to check to which assembly it belongs (the proxy type will not belong to one of your assemblies), for searching the first type in the hierarchy belonging to your domain model assembly(ies).
Note that if the proxy is a proxy of a base class, not of a concrete class, and your helper method is set as private, it will yield the base class type, without initializing the proxy. Performance-wise, this is better. While a virtual GetUnproxiedType simply returning GetType would return the concrete class type with current proxy factories, but it would also initialize the proxy.


We use NH 2 and this example did not work for us. (It FAILED to unproxy the type and left proxy type, see below). It said that 2 entities with the same id are not equal, when one of them is proxy(of COrganization) and other is not(DOrganization). When we had a hierarchy:

class Organization
class AOrganization : Organization
class COrganization : Organization
{
  public virtual COrganization GetConcrete()
  {
    return null;
  }
}

class DOrganization : COrganization
{
  public virtual COrganization GetConcrete()
  {
    return this;
  }
}

AOrganization aOrganization;
COrganization cOrganization;
contract = new CContract(aOrganization, cOrganization as COrganization); //(COrganization)(cOrganization.GetConcrete()),

So CContract has a field of type COrganization. With a setter

public class Contract: Entity <short>
{
    public virtual COrganization COrganization
    {
        get { return cOrganization; }
        protected internal set
        {
            if (cOrganization != null && value != cOrganization) // != calls ==, which calls Equals, which calls GetUnproxiedType()
                    throw new Exception("Changing organization is not allowed.");
            }
            cOrganization = value;
        }
    }
    private COrganization cOrganization;
}

We constructed new Contract, its constructor set the COrganization field pointing to some organization. Then we called UnitOfWork.Commit, NH tried to set COrganization field again (with the same id), GetUnproxiedType worked incorrectly, new and old values were recognized as non-equal, and exception was thrown...

Here is the place where error showed up:

            var otherType = other.GetUnproxiedType();
            var thisType = GetUnproxiedType();

            return thisType.IsAssignableFrom(otherType) ||
            otherType.IsAssignableFrom(thisType);

In debugger: otherType == COrganizationProxy - GetUnproxiedType failed... thisType == DOrganization

COrganizationProxy and DOrganization both inherit COrganization. So they are not IsAssignableFrom for each other...

Why does this example work for you?

Maybe because we have NH 2.0 or 2.1?

Or because of simple "cOrganization as COrganization" instead of "(COrganization)(cOrganization.GetConcrete())"?

Or because we have implementation of ==, != and Equals not only in Entity, but in Organization too?

public abstract class Organization : Entity<int>
{
    public override bool Equals(object obj)
    {
        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    public static bool operator ==(Organization object1, Organization object2)
    {
        return AreEqual(object1, object2);
    }

    public static bool operator !=(Organization object1, Organization object2)
    {
        return AreNotEqual(object1, object2);
    }
}

public abstract class Entity<TId>
{
    public virtual TId Id { get; /*protected*/ set; }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity<TId>);
    }

    private static bool IsTransient(Entity<TId> obj)
    {
        return obj != null &&
        Equals(obj.Id, default(TId));
    }

    private Type GetUnproxiedType()
    {
        return GetType();
    }

    public virtual bool Equals(Entity<TId> other)
    {
        if (other == null)
            return false;
        if (ReferenceEquals(this, other))
            return true;
        if (!IsTransient(this) &&
        !IsTransient(other) &&
        Equals(Id, other.Id))
        {
            var otherType = other.GetUnproxiedType();
            var thisType = GetUnproxiedType();
            return thisType.IsAssignableFrom(otherType) ||
            otherType.IsAssignableFrom(thisType);
        }
        return false;
    }

    public override int GetHashCode()
    {
        if (Equals(Id, default(TId)))
            return base.GetHashCode();
        return Id.GetHashCode();
    }

    /// This method added by me
    /// For == overloading
    protected static bool AreEqual<TEntity>(TEntity entity1, TEntity entity2)
    {
        if ((object)entity1 == null)
        {
            return ((object)entity2 == null);
        }
        else
        {
            return entity1.Equals(entity2);
        }
    }

    /// This method added by me
    /// For != overloading
    protected static bool AreNotEqual<TEntity>(TEntity entity1, TEntity entity2)
    {
        return !AreEqual(entity1, entity2);
    }
}
0

精彩评论

暂无评论...
验证码 换一张
取 消