Entity equality

In .NET, two objects are supposed to be equal when they both point to the same object in memory. This is also called reference equality. This is what the Equals method on System.Object implements. Reference equality works for normal .NET objects but is not adequate for persistent entities. Two persistent entities can be said to be equal if they point to the same database record. So if you have loaded two instances of same employee record then they are not equal on reference equality measures, but for NHibernate they are equal.

Why does equality matter?

Equality obviously matters if you want to compare two entities. But beyond the basics, equality matters because some basic collection operations depend on equality of objects. Calling the Add or Remove methods on ICollection in order to add or remove items to/from collection, internally depends on the equality check of the item being added. This is more important during removal of an item because some kind of equality check has to happen before the correct item to be removed is determined. For add, as long as the collection allows duplicates, no equality checks are required. For a collection represented using sets/maps which does not allow duplicates, every time a new item is added to the collection, equality check happens. Fortunately, NHibernate handles situations like these quite well if the entities involved are persistent entities loaded using the same session. But following are two situations where NHibernate cannot handle the equality and defers to .NET's implementation of equals (which defers to reference equality):

  • First situation is when the entities being compared are loaded using two different sessions resulting in two different instances in memory. This also applies when you are adding or removing items from collections outside of a session context. In a nutshell, the equality checks made outside of the context of a NHibernate session are not handled by NHibernate and hence defer to reference equality checks.
  • Second situation involves composite keys. We are yet to cover composite keys. A composite key is a primary key (or identity for NHibernate) which consists of more than one database column. Composite keys result in more than one property on entity acting as identifier. Even inside a session, NHibernate is not capable of determining equality of two instances of an entity that uses composite key. A non-composite identifier of some primitive types such as int, guid is what NHibernate can handle for equality checks.

Let's see with the following example how absence of proper equality checks may fail some operations. The code is written in the form of a unit test. We would then implement equality to make this test pass.

[Test]
public void SameEntityLoadedFromTwoDifferentSessionsMatches()
{
  var config = new DatabaseConfigurationForSqlServer();

  object id = 0;
  var employee1 = new Employee
  {
    EmployeeNumber = "123456789",
    DateOfBirth = new DateTime(1980, 2, 23),
    DateOfJoining = new DateTime(2012, 3, 15)
  };

  using (var session = config.OpenSession())
  {
    using (var transaction = session.BeginTransaction())
    {
      id = session.Save(employee1);
      transaction.Commit();
    }
  }

  Employee employee2 = null;
  using (var session = config.OpenSession())
  {
    using (var transaction = session.BeginTransaction())
    {
      employee2 = session.Get<Employee>(id);
      transaction.Commit();
    }
  }

  Assert.That(employee1.Equals(employee2));
}

In the preceding test, we have saved an instance of the Employee entity named employee1. This operation is carried out in a session and that session is then disposed. We then open a new session and load the same employee instance from database and assign it to a different local variable named employee2 this time. In the end, we compare the two instances using the Equals method. If you run the preceding test, it would fail saying employee1 and employee2 do not match. This happens because in absence of our custom equality checks, CLR defers to reference equality checks and being two different .NET objects, employee1 and emplpoyee2 fail on that count.

Note

In the preceding test, we have used a class named DatabaseConfigurationForSqlServer. This class builds NHibernate configuration against a SQL Server 2012 instance. We have been using SQLite for tests and that is the preferred database to use for tests. But SQLite has a limitation when used in in-memory mode; it does not support multiple sessions connecting to same database instance. Multiple sessions connecting to same database instance are exactly the thing we wanted for this test and hence I had to use SQL Server.

Next, we will see how to implement the custom equality checks that compare two persistent entities and consider them to be equal if they both point to the same database record.

Implementing equality

Two entities can be said to be equal if they both correspond to the same database record. In order to implement such a notion of equality for entities, we would need to override the following two methods on all our entities:

public int GetHashCode()
public bool Equals(object obj)

These methods are defined as virtual methods on System.Object. Before we implement these methods, let's take a moment and think about what we want to implement here. First and foremost, we want to make sure that the Id properties of the two entity instances have same value. We can extend the same logic for implementation of the GetHashCode() method. We can return hashcode of the Id property from our implementation of the GetHashCode method. But what if the entity is transient and does not have any id set? Since transient entities do not correspond to any database records, we can resort to .NET's reference equality check. So, we would use reference equality if entity is transient, else we would compare the identifier values of the two entity instances. That would give us the following implementation of these two methods:

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


public override bool Equals(object obj)
{
  var thisIsTransient = Id == 0;
  var otherIsTransient = other.Id == 0;

  if (thisIsTransient && otherIsTransient)
    return ReferenceEquals(this, other);

    return Id == other.Id;
}

There are two problems with the preceding code:

  • The object passed into the Equals method could be of any type. Even worse, it could be a null. We need to safeguard our code against such situations.
  • GetHashCode always returns hash code of the Id property. When a transient entity is saved and it is assigned an ID, the hashcode returned by this method would change. That is a violation of the GetHashCode implementation contract. Hashcode once assigned to an object must not be changed.

To fix the first problem, we just need to typecast the object passed into the Equals method to correct type and also check that it is not null. The second problem is more challenging to fix. We would need to generate hashcode for entity when it is transient and cache it. For this, we can fall back to .NET's default implementation of the GetHashCode method. Once the hashcode is generated and cached, we can return the same hashcode even when the entity becomes persistent. And if entity is already persistent (an entity loaded from database), we would cache and return the hashcode of its identifier. Following code listing shows this implementation:

private int? hashCode;
public override int GetHashCode()
{
  if (hashCode.HasValue) return hashCode.Value;

  var transientEntity = Id == 0;
  if (transientEntity)
  {
    hashCode = base.GetHashCode();
    return hashCode.Value;
    }
  return Id.GetHashCode();
}

Note the use of nullable int type to cache the hashcode. Another thing to note is that the preceding code uses a value of 0 for the identifier property to say that the entity is transient. This works for identifier strategies such as identity/hilo but would not work for other strategies such as guid or guidcomb, which do not generate an integer type of identifier value. Also, we have ignored a setting called unsaved-value declared during the mapping of identifier properties to specify the identifier value for transient entities. If this setting is declared to be other than 0 then preceding code needs to change accordingly.

Note

The above implementation still has a minor flaw. If the identifier of an entity changes then the GetHashCode contract is violated because the method now starts returning a different hashcode. While it is easy to cache the hashcode generated by calling Id.GetHashCode(), I would advise against it. I would rather suggest to try and not set identifier of a persistent entity to a different value. This is not ideal to do and can lead to subtle bugs.

In the code samples of this chapter, you will have seen that I have done additional refactoring of the above code. Because every entity needs implementation of equality, I moved these two methods into the EntityBase class. But then I could not anymore check the type of the object passed into the Equals method. To enable the type checking, I added a generic parameter on EntityBase to make it EntityBase<T>. A constraint is added to T that is must be a type that inherits from EntityBase<T> so that you can only pass domain entities as T. This way, we now know what is the type of the entity at runtime for which the Equals method is called.

Moreover, I have also overloaded the == and != operator. This comes in handy if you need to compare two instances of persistent entities in your code using these operators.

The modified code looks as follows:

namespace Domain
{
  public class EntityBase<T> where T : EntityBase<T>
  {
    private int? hashCode;
    public override int GetHashCode()
    {
      if (hashCode.HasValue) return hashCode.Value;

      var transientEntity = Id == 0;
      if (transientEntity)
      {
        hashCode = base.GetHashCode();
        return hashCode.Value;
      }
      return Id.GetHashCode();
    }

    public virtual int Id { get; set; }

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

      var thisIsTransient = Id == 0;
      var otherIsTransient = other.Id == 0;

      if (thisIsTransient && otherIsTransient)
      return ReferenceEquals(this, other);

      return Id == other.Id;
    }
    public static bool operator ==(EntityBase<T> lhs, EntityBase<T> rhs)
    {
      return Equals(lhs, rhs);
    }
    public static bool operator !=(EntityBase<T> lhs, EntityBase<T> rhs)
    {
      return !Equals(lhs, rhs);
    }
  }
}

Preceding code is all we need to make sure that two instances of entities in the memory pointing to same record in the database are treated as equal by NHibernate and CLR both. As we will see in Chapter 8, Using NHibernate in a Real-world Application, a generally recommended approach while developing web application using NHibernate is to use one session per incoming request. In such situation, most object equality needs are satisfied by NHibernate itself. But, it is still a good practice to implement the preceding code to ensure that you are not leaving some edge cases to chance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.103.154