Implementing versioning with NHibernate

I have recently been implementing a WCF service to act as a single repository for data that is common across a number of internal applications.  Often I have found that a 'Last in wins' approach has been sufficient for managing data updates.  However, in this case with multiple applications consuming the same data, and holding it for an indeterminate period of time before sending updates back, that was not going to be sufficient.  I was going to need to implement a 'First in wins' approach to prevent stale updates.  Since I was using NHibernate to manage access to the dataabase, I thought I would see what I get for free and was pleasantly surprised.

Before anyone raises any concerns, I would like to point out that I am using the standard pattern of domain and DTO objects.  The domain objects are mapped using NHibernate, and I am using an object-object mapper to manage the domain-DTO (and vice versa) conversion process.

So lets look at how it is done using a Phone object as an example

First, the script for the database table:
CREATE TABLE [dbo].[phone](
[phoneId] [uniqueidentifier] NOT NULL,
[number] [nvarchar](50) NULL,
[phoneType] [int] NULL,
[dateLastModified] [datetime] NULL,
[lastModifiedBy] [uniqueidentifier] NULL,
[version] [timestamp] NULL,
 CONSTRAINT [PK_phone] PRIMARY KEY CLUSTERED 
(
[phoneId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
I am using timestamp for the version field but I could have used an integer or a datetime.

Next, the phone object:
public class Phone : IPhone
{
    public virtual Guid PhoneId { get; set; }
    public virtual string Number { get; set; }
    public virtual PhoneType PhoneType { get; set; }
    public virtual IList ChangeHistory { get; set; }
    public virtual byte[] VersionNumber { get; set; }   
    public virtual IPerson LastModifiedBy { get; set; }
    public virtual DateTime? DateLastModified { get; set; }


    public Phone()
    { }
}
Note that I am storing the version number as a byte array.

The NHibernate mappings:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
  <class xmlns="urn:nhibernate-mapping-2.2" lazy="true" optimistic-lock="version" name="Phone, core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="phone">
    <id name="PhoneId" type="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="phoneId" />
      <generator class="guid" />
    </id>
    <version generated="always" name="VersionNumber" type="System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="version" />
    </version>
    <property name="Number" type="System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
      <column name="number" />
    </property>
    <property name="PhoneType" type="Int32">
      <column name="phoneType" />
    </property>
    <property name="DateLastModified" type="Timestamp">
      <column name="dateLastModified" not-null="false" />
    </property>
    <bag access="property" cascade="save-update" inverse="true" lazy="true" name="ChangeHistory">
      <key>
        <column name="phoneId" />
      </key>
      <one-to-many class="PhoneHistory, core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    </bag>
    <many-to-one cascade="none" class="Person, core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" lazy="proxy" name="LastModifiedBy">
      <column name="lastModifiedBy" />
    </many-to-one>
  </class>
</hibernate-mapping>

One gotcha here is that the version property needs to be set to generated=always.  Otherwise updates will fail.  Also note the optimistic lock setting

An example:
      Phone original = null;            
      using (ISession session = factory.OpenSession())
      {
          Guid id = new Guid("1D3FA6A7-129F-47D9-99A5-7ED955C99E43");
          original = session.Get(id);
          session.Close();
      }      


      using (ISession session = factory.OpenSession())
      {
          Guid id = original.PhoneId;
          Phone toChange = session.Get(id);                
          toChange.DateLastModified = DateTime.UtcNow;
          ITransaction tx = session.BeginTransaction();
          session.SaveOrUpdate(toChange);
          tx.Commit();
          session.Disconnect();
          session.Close();
      }            


      original.DateLastModified = DateTime.UtcNow.AddDays(-1);
      try
      {
          using (ISession session = factory.OpenSession())
          {
              session.Merge(original);
          }
      }
      catch (StaleObjectStateException staleEx)
      {
          //handle exception here
      }
Instead of letting the exception be created, you can load up the object and do a manual comparison yourself.  This won't have much of a performance overhead as using Merge will load the object anyway to see if there are changes to merge.

Comments