Wednesday 2 May 2012

Changing the Type of an Entity - Persistence with NHibernate

This is a follow up to my previous post which can be found here

So we have an Employee class and a Manger subclass, here are the classes:
  
    public class Employee : Entity
    {
        private string _name;
        private string _payrollNumber;
        private DateTime _joinedOn;

        public virtual string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        public virtual string PayrollNumber
        {
            get { return _payrollNumber; }
            set { _payrollNumber = value; }
        }

        public virtual DateTime JoinedOn
        {
            get { return _joinedOn; }
            set { _joinedOn = value; }
        }

        public static Employee Create(string role, string name, string payrollNumber, string department)
        {
            Employee employee = null;

            if (role == "Manager")
            {
                employee = new Manager();
                Manager manager = employee as Manager;
                manager.Department = department;
                manager.BecameManagerOn = DateTime.Now;
            }
            else
            {
                employee = new Employee();
            }

            employee.Name = name;
            employee.PayrollNumber = payrollNumber;
            employee.JoinedOn = DateTime.Now;
            return employee;
        }

        public void Amend(string name, string payrollNumber, DateTime joinedOn)
        {
            _name = name;
            _payrollNumber = payrollNumber;
            _joinedOn = joinedOn;
        }

        public virtual Manager Promote(string department)
        {
            Manager manager = new Manager();
            manager.Id = _id;
            manager.Name = _name;
            manager.PayrollNumber = _payrollNumber;
            manager.JoinedOn = _joinedOn;
            manager.Department = department;
            manager.BecameManagerOn = DateTime.Now;
            return manager;
        }

    }

    public class Manager : Employee
    {
        private string _department;
        private DateTime _becameManagerOn;

        public virtual string Department
        {
            get { return _department; }
            set { _department = value; }
        }

        public DateTime BecameManagerOn
        {
            get { return _becameManagerOn; }
            set { _becameManagerOn = value; }
        }
    }

    public class Entity
    {
        protected long? _id;
        protected int? _hashCode;

        public virtual long? Id
        {
            get { return _id; }
            set { _id = value; }
        }

        public override bool Equals(object obj)
        {
            Entity other = obj as Entity;
            if (other == null) return false;
            if (!other.Id.HasValue && !_id.HasValue) return other == this;
            if (!other.Id.HasValue || !_id.HasValue) return false;
            return other.Id.Value == _id.Value;
        }

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

            if (!_id.HasValue)
            {
                _hashCode = base.GetHashCode();
                return _hashCode.Value;
            }

            return _id.GetHashCode();
        }
    }
And here are how they are mapped in NHibernate:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="HRSystem.Domain.Entities"
                   assembly="HRSystem.Domain">
  <class name="Employee" table="`Employee`">
    
    <id name="Id" column="Id" type="long">
      <generator class="identity"/>
    </id>
    
    <property name="Name" type="string" />
    <property name="PayrollNumber" type="string" />
    <property name="JoinedOn" type="datetime" />

    <joined-subclass name="Manager" table="`Manager`">
      <key column="`Id`"/>
      <property name="Department" type="string" />
      <property name="BecameManagerOn" type="datetime" />
    </joined-subclass>
    
  </class>
</hibernate-mapping>
A common question on Stack Overflow is about changing from one class to an inherited subclass in NHibernate. Here are some examples:

Convert type of entity to subtype with NHibernate/Hibernate
Can you change from a base class to a joined subclass type in nhibernate?
Is it ever valid to convert an object from a base class to a subclass
NHibernate - Changing sub-types

One common response is that one should use composition over inheritance. In the case of our Employee/manager example, this would mean (for example) that everyone in the company would be represented by an Employee entity. Each Employee entity would have a Role entity which would be subclassed to the respective role (BasicEmployee, Manager, etc), and any varying behaviour is deferred to the Role class. see Design Patterns by the Gang Of Four for more about favouring composition over inheritance.

This is the correct approach when it is the behaviour that varies, but in our case, Manager has some extra fields, and composition over inheritance doesn't account for this. We need somewhere to store the extra fields, and inheritance is a good solution to this.

Some people suggest that the employee object should have a ManagerFields objects, where fields specific to a manager role ('_department', '_becameManagerOn') would be stored if the employee is a manager, and would be null if not. Or even, just have nullable fields '_department' and '_becameManagerOn' on Employee that are only used if the Employee is a manager. You have to ask yourself if you would do this if you were not using NHibernate?

Some people suggest that if you are using inheritance, then to promote an Employee to Manager, you should use native SQL to insert a record into the manager table, so when you next get the Employee it will cast to manager. This doesn't even warrant discussion.

The answer, that some people touch on on in the StackOverflow questions, is that you need to to delete the old entity, and replace it with the new entity. This seems quite straightforward, but there is one aggrevating factor: often we would want it to keep the same ID, as this may be a foreign key in other entities. (This is not a problem if the entity being changed is a child in an aggregate, as the aggregate root would have all the references, and would be doing all the updating so could update these references.) But what if it is an aggregate root? What if other entities in the domain, or even other domains/systems reference it? We need a way to preserve the ID.

Replacing Entities While Maintaining the Identity - GUIDs


One answer is to use GUIDs instead of auto incrementing IDs, this way assigned identites can be used. In this situation, it is not too much of a problem to delete the old Employee entity and save the new Manager entity which has the same ID. This way, when any entity is loaded that references the Employee, it will load the Manager entity. The code for the base entity would now look like this:
    public class Entity
    {
        protected Guid? _id;    //Changed from long?
        protected int? _hashCode;

        public virtual Guid? Id    //Changed from long?
        {
            get { return _id; }
            set { _id = value; }
        }

        //Implementation of Equals() and GetHaskCode() etc...
    }
The mapping files now look like this:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   namespace="HRSystem.Domain.Entities"
                   assembly="HRSystem.Domain">
  <class name="Employee" table="`Employee`">
    
    <id name="Id" column="Id" type="guid">
      <generator class="assigned"/>
    </id>
    
    <property name="Name" type="string" />
    <property name="PayrollNumber" type="string" />
    <property name="JoinedOn" type="datetime" />

    <joined-subclass name="Manager" table="`Manager`">
      <key column="`Id`"/>
      <property name="Department" type="string" />
      <property name="BecameManagerOn" type="datetime" />
    </joined-subclass>
    
  </class>
</hibernate-mapping>
Remember what the Promote code lookes like?
        public virtual Manager Promote(string department)
        {
            Manager manager = new Manager();
            manager.Id = _id;       //Especially pay attention to this line!!!
            manager.Name = _name;
            manager.PayrollNumber = _payrollNumber;
            manager.JoinedOn = _joinedOn;
            manager.Department = department;
            manager.BecameManagerOn = DateTime.Now;
            return manager;
        }
Now the code for a service to promote an Employee to a Manager looks like this:
using System;
using NHibernate;
using NHibernate.Cfg;
using HRSystem.Domain.Entities;

namespace HRSystem.Application.Implementations
{
    public class EmployeeService
    {
        public void Promote(Guid employeeId, string department)
        {
            Configuration configuration = new Configuration();
            configuration.Configure();
            configuration.AddAssembly("HRSystem.Data");
            ISessionFactory sessionFactory = configuration.BuildSessionFactory();
            ISession session = sessionFactory.OpenSession();
            Employee employee = session.Get<Employee>(employeeId);
            Manager manager = employee.Promote(department);

            using (ITransaction transaction = session.BeginTransaction())
            {
                session.Delete(employee);
                session.Save(manager);
                transaction.Commit();
            }
        }
    }
}
This is all quite straigntforward. This application service loads the Employee, gets a Manager version of it, deletes the Employee and then Saves the Manager in its place, with the same Id. Now when you go to load an Employee with the same Id, it will return an entity of type Manager. No need to update any other entities that have references to the entity because they will load the new Manager entity, because the ID has not changed.

Replacing Entities While Maintaining the Identity - Auto Incrementing IDs


The problem comes when you are trying to do this in system that uses auto-incrementing ints or bigints as IDs. This is when things start to get a bit messy. You need to do two things: first you need to dynamically manipulate the NHibernate configuration so the Id of the Employee class can be assigned. Secondly, we need to allow the identity to be inserted in the database, and the only way to do this (that I know of) is to use native SQL. Assume the Entity class and mapping file have reverted back to using auto-increment IDs, the way they were at the beginning of this post. The resulting code for the application service now looks like this:
using System;
using NHibernate;
using NHibernate.Cfg;
using HRSystem.Domain.Entities;

namespace HRSystem.Application.Implementations
{
    public class EmployeeService
    {
        public void Promote(Guid employeeId, string department)
        {
            Configuration configuration = new Configuration();
            configuration.Configure();
            configuration.AddAssembly("HRSystem.Data");

            //Get the class mapping for the Employee entity.
            var employeeClassMapping = (from classMapping 
                     in configuration.ClassMappings 
                     where classMapping.MappedClass == typeof(Employee)
                     select classMapping).FirstOrDefault();

            //Change the identifier strategy to assigned dynamically.
            (employeeClassMapping.Identifier as SimpleValue).IdentifierGeneratorStrategy = "assigned";

            ISessionFactory sessionFactory = configuration.BuildSessionFactory();
            ISession session = sessionFactory.OpenSession();
            Employee employee = session.Get<Employee>(employeeId);
            Manager manager = employee.Promote(department);

            using (ITransaction transaction = session.BeginTransaction())
            {
                //First delete the Employee entity.
                session.Delete(employee);

                //Native SQL to allow identity insert.
                session.CreateSQLQuery("SET IDENTITY_INSERT Employee ON").ExecuteUpdate(); 

                //Then save the Manager entity.
                session.Save(manager);

                //This is needed to ensure manager is saved while identity insert is on.
                session.Flush(); 

                //Native SQL to disallow identity insert.
                session.CreateSQLQuery("SET IDENTITY_INSERT Employee OFF").ExecuteUpdate(); 

                transaction.Commit();
            }
        }
    }
}
I never said it would be pretty but it works.

No comments:

Post a Comment