NHibernate: причины переопределения равных и GetHashCode

Есть ли причины, по которым Equals или GetHashCode должны быть переопределены в сущностях при использовании NHibernate? И в каких сценариях эти причины действительны?

Некоторые причины, которые можно найти в Интернете:

  • Поддержка ленивой загрузки. Сравнение прокси-объектов через значение по умолчанию метод может привести к неожиданным ошибкам. Но это должно быть разрешено (и оно действительно находится в во многих случаях), не так ли? При работе с сущностями с одного сеанса все должно работать нормально даже без переопределения Equals/GetHashCode. Здесь любые случаи, когда тождественное отображение не играй хорошо, это роль?
  • Это важно для коллекций NHibernate. Существуют ли случаи, когда реализация GetHashCode по умолчанию недостаточна (не включая вопросы, связанные с равными)?
  • Смешивание объектов из нескольких сессий и отдельных объектов. Это хорошая идея сделать это?

Любые другие причины?

Ответы

Ответ 1

Перегрузка методов Equals и GetHashCode важна, если вы работаете с несколькими сеансами, отдельными объектами, сессиями или коллекциями без состояния (см. ответ Sixto Saez для примера!).

В той же самой идентификационной карте области сеанса убедитесь, что у вас есть только один экземпляр того же объекта. Тем не менее, существует возможность сравнения объекта с прокси-сервером одного и того же объекта (см. Ниже).

Ответ 2

Как вы упомянули в своем вопросе, идентификатор экземпляра сущности является основным требованием для переопределения Equals и GetHashCode. В NHibernate наилучшей практикой является использование значений числового ключа (короткий, int или long, если это необходимо), поскольку упрощает сопоставление экземпляра с строкой базы данных. В мире базы данных это числовое значение становится значением столбца первичного ключа. Если таблица имеет то, что называется естественным ключом (где несколько столбцов вместе однозначно идентифицируют строку), тогда одно числовое значение может стать суррогатным основным ключом для этой комбинации значений.

Если вы определяете, что не хотите использовать или не можете использовать один числовой первичный ключ, вам необходимо сопоставить идентификатор с помощью NHibernate Функциональность CompositeKey. В этом случае вам абсолютно необходимо реализовать пользовательские переопределения GetHashCode и Equals, поэтому логика проверки значения столбца для этой таблицы может определять идентификатор. Вот хорошая статья о переопределении методов GetHashCode и Equals. Вы также должны переопределить равный оператор, который будет полным для всех применений.

Из комментария: В каких случаях реализация по умолчанию EqualsGetHashCode) недостаточна?

Реализация по умолчанию не подходит для NHibernate, потому что она основана на Object.Equals реализации. Этот метод определяет равенство для ссылочных типов как ссылочное равенство. Другими словами, эти два объекта указывают на одну и ту же ячейку памяти? Для NHibernate равенство должно основываться на значении (ах) тождественного отображения.

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

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);
    }
}