Lazy-loaded NHibernate properties in Equals and GetHashCode

廉价感情. 提交于 2019-11-30 15:21:46
asgerhallas

Two entities are equal if they are of the same type and has the same primary key.

If you have integers for keys:

  1. Check for reference equality like you do now
  2. If you have the Equal method in some base class you check that the types you're comparing are equal. Here you can get in to trouble with proxies, I'll return to that
  3. Check if the primary keys are equal - that will not cause any lazy-loading

If you have GUIDs for keys:

  1. Check for reference equality like you do now
  2. Check if the primary keys are equal - that will not cause any lazy-loading

If I have integers for keys I usually have something like this Equal-override in a base class for my entities:

public virtual bool Equals(EntityBase other)
{
    if (other == null)
    {
        return false;
    }

    if (ReferenceEquals(other, this))
    {
        return true;
    }

    var otherType = NHibernateProxyHelper.GetClassWithoutInitializingProxy(other);
    var thisType = NHibernateProxyHelper.GetClassWithoutInitializingProxy(this);
    if (!otherType.Equals(thisType))
    {
        return false;
    }

    bool otherIsTransient = Equals(other.Id, 0);
    bool thisIsTransient = Equals(Id, 0);
    if (otherIsTransient || thisIsTransient)
        return false;

    return other.Id.Equals(Id);
}

Now if you entities that inherit from others using table per hierarchy you will face the problem that GetClassWithoutInitializingProxy will return the base class of the hierarchy if it's a proxy and the more specific type if it's a loaded entity. In one project I got around that by traversing the hierarchy and thus always comparing the base types - proxy or not.

In these days though I would always go for using GUIDs as keys and do as described here: http://nhibernate.info/doc/patternsandpractices/identity-field-equality-and-hash-code.html

Then there is no proxy type mismatch problem.

If you are using identity equality, you should be able to access the key without triggering a load:

public virtual bool Equals(ClassB other)
{
    if (ReferenceEquals(null, other))
    {
        return false;
    }
    if (ReferenceEquals(this, other))
    {
        return true;
    }
    // needs to check for null Id
    return Equals(other.ClassC.Id, ClassC.Id) && Equals(other.ClassD.Id, ClassD.Id);
}

You can handle comparisons between objects before and after persisting by caching the hash code when it was transient. This leaves a small gap in the Equals contract in that a comparison between an existing object that was transient will not generate the same hash code as a newly-retrieved version of the same object.

public abstract class Entity
{
    private int? _cachedHashCode;

    public virtual int EntityId { get; private set; }

    public virtual bool IsTransient { get { return EntityId == 0; } }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }
        var other = obj as Entity;
        return Equals(other);
    }

    public virtual bool Equals(Entity other)
    {
        if (other == null)
        {
            return false;
        }
        if (IsTransient ^ other.IsTransient)
        {
            return false;
        }
        if (IsTransient && other.IsTransient)
        {
            return ReferenceEquals(this, other);
        }
        return EntityId.Equals(other.EntityId);
    }

    public override int GetHashCode()
    {
        if (!_cachedHashCode.HasValue)
        {
            _cachedHashCode = IsTransient ? base.GetHashCode() : EntityId.GetHashCode();
        }
        return _cachedHashCode.Value;
    }
}

I use the following rules:

  1. If entity has a POID property (remember that there is not need of property or any member just omit the name="XX", not sure if activerecord or the mapping strategy you are using supoprt this)

    • Not transient: If instance has ID != default(idType) then it is equals to another entity if both have the same id.
    • Transient: If instance has ID == default(idType) then it is equals to another entity if both are the same Reference. ReferenceEquals(this, other).
  2. If entity doesn't have a POID property, for sure you will need a natural-id. Use natural id for equality and GetHashCode.

  3. If you have a natural-id with many-to-one, instead of doing FooProperty.Equals(other.FooProperty), use FooProperty.Id.Equals(other.FooProperty.Id). Accessing the ID doesn't trigger the initialization of the lazy reference.

Last but not least, using composite-id is discourage, and composite id with key-many-to-one is very discourage.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!