16

Are there any reasons why Equals or GetHashCode should be overriden in entities when using NHibernate? And in which scenarios are these reasons valid?

Some reasons that can be found on web:

  • Support for lazy loading. Comparing proxy objects via default Equals method can lead to unexpected bugs. But this should be resolved by identity map (and it really is in many cases), should not it? When working with entities from single session everything should work fine even without overriding Equals/GetHashCode. Are there any cases when the identity map does not play well it‘s role?
  • It is important for NHibernate collections. Are there any cases when default implementation of GetHashCode is not sufficient (not including Equals related issues)?
  • Mixing entities from several sessions and detached entities. Is it good idea to do so?

Any other reasons?

Jakub Linhart
  • 4,062
  • 1
  • 26
  • 42

2 Answers2

12

As you mention in your question, identity of an entity instance is the main requirement for overriding Equals & GetHashCode. It is a best practice in NHibernate to use numeric key values (short, int, or long as appropriate) because it simplifies mapping an instance to a database row. In the database world, this numeric value becomes the primary key column value. If a table has what is called a natural key (where several columns together uniquely identify a row) then a single numeric value can become a surrogate primary key for this combination of values.

If you determine that you don't want to use or are prevented from using a single numeric primary key then you'll need to map the identity using the NHibernate CompositeKey functionality. In this case, you absolutely need to implement custom GetHashCode & Equals overrides so the column value checking logic for that table can determine identity. Here is a good article on overriding the GetHashCode and Equals method. You should also override the equal operator to be complete for all usages.

From the comment: In which cases is default implementation of Equals (and GetHashCode) insufficient?

The default implementation is not good enough for NHibernate because it is based on the Object.Equals implementation. This method determines equality for reference types as reference equality. In other words, are these two objects pointing to the same memory location? For NHibernate, the equality should be based on the value(s) of the identity mapping.

If you don't, you will most likely run into comparing a proxy of an entity with the real entity and it will be miserable to debug. For example:

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!).
}

Here is an example base class if you're using int as your Id (which could also be abstracted to any identity type):

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);
    }
}
Community
  • 1
  • 1
Sixto Saez
  • 12,610
  • 5
  • 43
  • 51
  • 1
    Thanks for your answer. My question is not clear enough. In which cases is default implementation of Equals (and GetHashCode) insufficient? When working with entities that are attached to a single session then everything should go well thanks to NH identity cache unless the identity cache is broken somehow (and I'm curious why the cache could be broken). Mixing detached entities and entities retrieved via several session is something like running with scissors for me:). When is it necessary to do such thing? – Jakub Linhart May 02 '11 at 14:29
  • The Remove() lacks a small detail, it is not clear if Blog is attached to the current session. If Blog is attached, the remove should work. This code works without implementing Equals/GetHashCode: using (var session = CreateSession()) { var post0 = session.Get(postId0); var blog = session.Load(blogId); Assert.IsTrue(blog.Posts[0] is INHibernateProxy); Assert.IsFalse(blog.Posts[1] is INHibernateProxy); blog.Posts.Remove(post0); session.Flush(); – Fried Jan 08 '20 at 07:44
7

Overloading the Equals and GetHashCode methods is important if you are working with multiple sessions, detached entities, stateless sessions or collections (see Sixto Saez's answer for an example!).

In the same session scope identity map will ensure that you only have a single instance of the same entity. However, there is the possibility of comparing an entity with a proxy of the same entity (see below).

TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
Dmitry S.
  • 8,373
  • 2
  • 39
  • 49
  • Thanks, it is pleasure to hear such answer:). Are you sure, that the session identity map is not broken in some corner cases? Do you have any example when working with several sessions is unavoidable? – Jakub Linhart May 03 '11 at 07:47
  • 2
    You should never get 2 different objects that represent that same entity (as in the same class type and the same database row) in the same session. Otherwise, it would be a bug in NHibernate. Sometimes you may use a stateless session (independently from the main session) for something like batch insert/update operations or writing audit data into the database. For example you cannot use your main session inside persistence interceptors. – Dmitry S. May 03 '11 at 14:51
  • 1
    @DmitryS. you *can* get two objects that represent the same entity: a proxy entity in a collection and non-proxy entity. If the non-proxy refers to the collection entity, reference equality (the default in NHibernate) **fails**. Therefore, it is always recommended to override equals. – TheCloudlessSky Nov 20 '13 at 23:46
  • "However, there is the possibility of comparing ..." - until an opposite example - I disagree. If I load an (child-) entity as a proxy and later on I access the parents objects child-collection, the child collection contains the proxy object and the other NonProxy-Objects of the collection. Proved with NHibernate 5 and bags. Maybe another senario may deliver other results ... I fully agree in "In the same session scope identity map will ensure that you only have a single instance of the same entity." – Fried Jan 08 '20 at 07:52