Как мы должны действительно внедрять Equals и GetHashCode для объектов NHibernate

Есть много вопросов, ответов и статей на этот вопрос, но, на мой взгляд, нет реального четкого/правильного ответа.

Для меня Айенде имеет наилучшую общую реализацию, которую я видел: http://ayende.com/blog/2500/generic-entity-equality

.... Но это с 2007 года.

Является ли это "лучшим способом" для реализации этих методов, особенно в отношении NHibernate 3.2, который содержит некоторые отличия в реализации прокси до более ранних версий?

Ответ 1

Да!

Вы должны переопределить Equals и GetHashCode. Но вы не должны выполнять равенство ценности (Name == other.Name && Age == other.Age), вы должны делать равенство идентичности!

Если вы этого не сделаете, вы, скорее всего, столкнетесь с сравнением прокси-объекта с реальным сущностью, и он будет несчастным отлаживать. Например:

public class Blog : EntityBase<Blog>
{
    public virtual string Name { get; set; }

    // This would be configured to lazy-load.
    public virtual IList<Post> Posts { get; protected set; }

    public Blog()
    {
        Posts = new List<Post>();
    }

    public virtual Post AddPost(string title, string body)
    {
        var post = new Post() { Title = title, Body = body, Blog = this };
        Posts.Add(post);
        return post;
    }
}

public class Post : EntityBase<Post>
{
    public virtual string Title { get; set; }
    public virtual string Body { get; set; }
    public virtual Blog Blog { get; set; }

    public virtual bool Remove()
    {
        return Blog.Posts.Remove(this);
    }
}

void Main(string[] args)
{
    var post = session.Load<Post>(postId);

    // If we didn't override Equals, the comparisons for
    // "Blog.Posts.Remove(this)" would all fail because of reference equality. 
    // We'd end up be comparing "this" typeof(Post) with a collection of
    // typeof(PostProxy)!
    post.Remove();

    // If we *didn't* override Equals and *just* did 
    // "post.Blog.Posts.Remove(post)", it'd work because we'd be comparing 
    // typeof(PostProxy) with a collection of typeof(PostProxy) (reference 
    // equality would pass!).
}

Вот пример базового класса, если вы используете int как ваш Id (который также можно абстрагировать до любого типа идентификации):

public abstract class EntityBase<T>
    where T : EntityBase<T>
{
    public virtual int Id { get; protected set; }

    protected bool IsTransient { get { return Id == 0; } }

    public override bool Equals(object obj)
    {
        return EntityEquals(obj as EntityBase<T>);
    }

    protected bool EntityEquals(EntityBase<T> other)
    {
        if (other == null)
        {
            return false;
        }
        // One entity is transient and the other is not.
        else if (IsTransient ^ other.IsTransient)
        {
            return false;
        }
        // Both entities are not saved.
        else if (IsTransient && other.IsTransient)
        {
            return ReferenceEquals(this, other);
        }
        else
        {
            // Compare transient instances.
            return Id == other.Id;
        }
    }

    // The hash code is cached because a requirement of a hash code is that
    // it does not change once calculated. For example, if this entity was
    // added to a hashed collection when transient and then saved, we need
    // the same hash code or else it could get lost because it would no 
    // longer live in the same bin.
    private int? cachedHashCode;

    public override int GetHashCode()
    {
        if (cachedHashCode.HasValue) return cachedHashCode.Value;

        cachedHashCode = IsTransient ? base.GetHashCode() : Id.GetHashCode();
        return cachedHashCode.Value;
    }

    // Maintain equality operator semantics for entities.
    public static bool operator ==(EntityBase<T> x, EntityBase<T> y)
    {
        // By default, == and Equals compares references. In order to 
        // maintain these semantics with entities, we need to compare by 
        // identity value. The Equals(x, y) override is used to guard 
        // against null values; it then calls EntityEquals().
        return Object.Equals(x, y);
    }

    // Maintain inequality operator semantics for entities. 
    public static bool operator !=(EntityBase<T> x, EntityBase<T> y)
    {
        return !(x == y);
    }
}

Ответ 2

Моя личная рекомендация заключается не в том, чтобы реализовать эти методы вообще, потому что это приводит к загрузке во многих случаях, когда это действительно не нужно.

Кроме того, если вы не перемещаете объекты через сеансы, вам это никогда не понадобится. И даже если вы это сделаете, вы всегда можете сравнить по идентификатору, когда это необходимо.

Ответ 3

У меня было несколько проблем при реализации решения, предложенного @TheCloudlessSky.

Во-первых, мои идентификаторы не имеют согласованного типа данных; некоторые из них int, некоторые из них Guid, а некоторые - string. Кроме того, некоторые из них автоматически генерируются, а другие назначаются вручную. Другая проблема может возникнуть в будущем, если я решит использовать составные идентификаторы. Следовательно, я не могу положить

public virtual int Id { get; protected set; }

в EntityBase базовом классе. Я должен определить его в соответствующих конкретных классах Entity.

Во-вторых, поскольку я не могу иметь Id в базовом классе, было сложнее реализовать свойство bool IsTransient.

Итак, я решил создать Guid для каждого экземпляра и использовать его для реализации GetHashCode и Equals, как показано ниже:

public abstract class BaseEntity
{
    Guid objectId = Guid.NewGuid();
    public virtual Guid ObjectId { get { return objectId; } }

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

    public override bool Equals(object other)
    {
        if(other == null)
            return false;
        if(ObjectId != (other as BaseEntity).ObjectId)
            return false;
        return ReferenceEquals(this, other);
    }
}