NHibernate - Getting up and running

Overriding Equals, GetHashCode and Database Design with NHibernate

Index

Introduction to NHibernate
Simple Queries on a single table with NHibernate
Configuring Child Collections with NHibernate
Overriding Equals, GetHashCode and Database Design with NHibernate
Querying Collections Efficiently with NHibernate
Persisting objects back to the database with NHibernate

Introduction

As our ORM application is starting to grow, its good time to look at one of NHibernate’s best practices, which is to override the Equals and GetHashCode methods for our business objects. There are a number of reasons for this as stated by the documentation. They include

  • Required for object comparison outside of the ISession scope
  • Required for composite identifier equality
  • Without, Memory leaks will occur in custom IResultTransformers

While these topics have not been discussed, its just a good habit to get into.

Defining object equality

The Id (primary key) field seems a natural candidate, and is often used for business equality. This is not recommended for the following reason

A transient object doesn't have an identifier value and NHibernate would assign a value when the object is saved. If the object is in an ISet while being saved, the hash code changes, breaking the contract. To implement Equals() and GetHashCode(), use a unique business key, that is, compare a unique combination of class properties.

Given the above, the methods have been overridden with the fields below.

Employee

        public override bool Equals(object obj) {
            if (obj == null || GetType() != obj.GetType())
                return false;

            Employee e = (Employee)obj;

            return 
                string.Compare(FirstName, e.FirstName, true) == 0 &&
                string.Compare(LastName, e.LastName, true) == 0 &&
                DateTime.Equals(BirthDate, e.BirthDate);
        }

        public override int GetHashCode() {

            // Jon Skeet's recommended implementation
            unchecked
            {
                int hash = 17;

                hash = hash * 23 + (string.IsNullOrEmpty(FirstName) ? 
                    string.Empty.GetHashCode() : 
                    FirstName.GetHashCode());

                hash = hash * 23 + (string.IsNullOrEmpty(LastName) ? 
                    string.Empty.GetHashCode() : 
                    LastName.GetHashCode()); 

                hash = hash * 23 + BirthDate.GetHashCode();

                return hash;
            }
        }

Order

        public override bool Equals(object obj) {
            if (obj == null || GetType() != obj.GetType())
                return false;

            Order o = (Order)obj;
            return
                (Employee == o.Employee) &&
                (CustomerId == o.CustomerId) &&
                (OrderDate == o.OrderDate);
        }

        public override int GetHashCode() {

            // Jon Skeet's recommended implementation
            unchecked 
            {
                int hash = 17;

                hash = hash * 23 + (null == Employee ? 
                    new Employee().GetHashCode() : 
                    Employee.GetHashCode());

                hash = hash * 23 + (string.IsNullOrEmpty(CustomerId) ? 
                    string.Empty.GetHashCode() : 
                    CustomerId.GetHashCode());

                hash = hash * 23 + OrderDate.GetHashCode();

                return hash;
            }
        }

This is obviously brittle code, while it’s unlikely that a company will have an employee with the same name and birthdate, its more than likely that this employee fill more than one order for a given customer on a given day.

Unfortunately, we are working within the constraints of the Northwind database. With a real world database the schema should look more like this

Employee primary key on the Employees table

Here, the EmployeeID is the database key and has no business meaning, while the PayrolNo is often known and quoted by employees (it often appears on their security passes)

Order primary key on the Orders table

Again, with the Order table, the OrderID is the database key and the OrderNo would often be quoted by the accounts department.

With this refactored schema, PayrollNo and OrderNo would lend themselves to object equality tests.

Note: A business key should never be used as a primary key, for starters it’s uniqueness is not guaranteed and if the business process/model ever changes you’ll have a major problem on your hands. Surrogate keys of an optimal data type should always be used. (This is a valuable lesson in data warehouse design)

Summary

The key points to take away from the above are

  • Always override the Equals and GetHashCode methods for your business objects
  • Although tempting, do not use the Id field of your object due to its transient state
  • Never use business keys as database keys, use surrogates

Source Code

git clone https://github.com/mindfulsoftware/ORM.Nhibernate.git
(version: 8b5af698bcc519a26310ccef780501e750bc5eab)