Making history explicit
Introduction
In one product of our solution we needed to record the full history of some entities. What does this mean? It means we create a history entry whenever the state of the corresponding entity changes.
When doing this there are basically two ways one can choose – either implicitly generate the history records or doing it explicitly. To go the implicit route we could have used an interceptor mechanism to automatically and transparently create a history record whenever the entity changes and the change is persisted to the database.
Since we chose to go the explicit route creating a history for an entity became a domain concept.
The domain
To show the concept I chose the sample of a Zoo whose management wants to keep track of the cages used throughout the facility as well as of the animals hosted in the cages. Let’s start with the simplified model of a cage and its cage history. In this model the cage entity represents the status quo that is the current state of the cage. Each cage has a collection of cage history entities which represent the full history of the cage from the moment it first appeared in the Zoo till the present day. Each cage history entity represents a snapshot of the cage’s state at a specific time in the past.
The current state (i.e. the cage) is important whenever the Zoo management wants to make an inventory (or census) of all cages and animals. On the other hand the history records (i.e. cage history entities) are important whenever the management wants to do retrospective reporting and/or billing.
As we can see in the above image a cage is of a certain cage type and is located at a specific location. The cage has many other properties which I do not show here to keep the sample as simple as possible. Now a cage is installed at some location but this location may change over time (imagine a fish tank that is move from floor A of building 1 to floor B. In such a situation a history record has to be generated.
To simplify things a little bit and to remain as DRY as possible Cage and CageHistory both derive from VersionedEntity
The VersionedEntity
Implementation of the model
xxx
public class Cage : VersionedEntity<Cage>
{
private int lastVersion;
private CageHistory currentHistory;
public Cage(CageType cageType, Location location)
{
lastVersion = int.MinValue;
CageType = cageType;
Location = location;
cageHistories = new List<CageHistory>();
CreateOrUpdateHistoryRecord();
}
protected Cage() { } // default constructor only needed to satisfy NHibernate
public virtual CageType CageType { get; private set; }
public virtual Location Location { get; private set; }
private IList<CageHistory> cageHistories;
public virtual IEnumerable<CageHistory> CageHistories { get { return cageHistories; } }
public virtual void ChangeLocation(Location newLocation)
{
Location = newLocation;
CreateOrUpdateHistoryRecord();
}
private void CreateOrUpdateHistoryRecord()
{
if (lastVersion != Version)
{
Version++;
currentHistory = new CageHistory();
cageHistories.Add(currentHistory);
lastVersion = Version;
}
currentHistory.CreateFromCage(this);
}
}</p>
We want to only deal with cage entities that are always in a valid state (but for simplicity I do not show the validation code). A cage object can be instantiated in exactly two ways either through the constructor (if its a new cage) which expects a cage type and a location as arguments or it can be instantiated by NHibernate and populated with data from the database (if it already exists). We assume that the data in the database is valid.
Creating or updating a history record
The method CreateOrUpdateHistoryRecord is responsible to either create a new or update an existing history record. Note that we create exactly one history record per unit of work if the cage entity is changed, independent of how many times the cage is changed.
In our sample code we have exactly two places where the state of a cage changes namely during creation (constructor) and when the location changes. Each time we call the CreateOrUpdateHistoryRecord method.
Why are all properties read-only?
In our applications we chose to not use property setters in the domain. Assigning values to properties of an entity is in many case not expressive enough. The assignment of a specific value to a given property is not intention revealing. The more complex a domain gets the more important is this fact. Thus we have methods to change state of an entity. In our simple sample this might be an overkill specifically since the method ChangeLocation only changes one property. But often we encounter situations where we have to change several interrelated properties in one atomic operation.
Let’s now have a look at the CageHistory class
public class CageHistory : VersionedEntity<CageHistory>
{
public virtual Cage Cage { get; set; }
public virtual CageType CageType { get; private set; }
public virtual Location Location { get; private set; }
public virtual void CreateFromCage(Cage cage)
{
Cage = cage;
Version = cage.Version;
CageType = cage.CageType;
Location = cage.Location;
}
}</p>
We have chosen the CageHistory to be responsible to create itself as a snapshot of the current state of the cage. The method CreateFromCage is responsible for this and can be considered to be a factory method.
Other than that the CageHistory entity is a mirror of the Cage entity regarding its properties. It also contains a reference back to the Cage itself.
Mapping of the model
To map our entities to the underlying database we choose Fluent NHibernate. To be more explicit we do not use the auto mapping functionality of Fluent NHibernate but rather use fluent mappings.</p>
public class CageMap : ClassMap<Cage>
{
public CageMap()
{
Id(x => x.Id).GeneratedBy.Assigned();
Map(x => x.Version).Not.Nullable();
References(x => x.CageType).Not.Nullable();
References(x => x.Location).Not.Nullable();
HasMany(x => x.CageHistories)
.Cascade.AllDeleteOrphan()
.Inverse()
.Access.CamelCaseField();
}
}</p>
public class CageHistoryMap : ClassMap<CageHistory>
{
public CageHistoryMap()
{
Id(x => x.Id).GeneratedBy.Assigned();
Map(x => x.Version).Not.Nullable();
References(x => x.Cage).Not.Nullable();
References(x => x.CageType).Not.Nullable();
References(x => x.Location).Not.Nullable();
}
}</p>
Again the above mapping should be straight forward.
Testing
In this post I do not want to show the basic test e.g. mapping test. These kind of tests are well described in the Fluent NHibernate wiki. What I want to show is how we can test whether the creation of history records is working as expected. To do the tests we use SqLite in InMemory mode as our database.</p>
For all database related test we can use a fixture base class like this
public abstract class database_fixture_base
{
private ISessionFactory sessionFactory;
protected ISession session;
private Configuration configuration;
[TestFixtureSetUp]
public void TestFixtureSetup()
{
sessionFactory = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.ShowSql().InMemory)
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<Cage>())
.ExposeConfiguration(c => configuration = c)
.BuildSessionFactory();
}
[SetUp]
public void Setup()
{
session = sessionFactory.OpenSession();
new SchemaExport(configuration).Execute(false, true, false, session.Connection, null);
}
[TearDown]
public void TearDown()
{
session.Dispose();
}
}</p>
Note how easy and expressibe the configuration of NHibernate becomes when using Fluent NHibernate. Before each test we create a new session and use this session to create the database schema by using the NHibernate SchemaExport class. After each test we dispose the session. The database schema is deleted when disposing the session when using SqLite in InMemory mode. This is exactly what we need to avoid any side effects from test to test.
[TestFixture]
public class cage_mapping_specs : database_fixture_base
{
[Test]
public void creating_a_cage_creates_a_cage_history()
{
var cageType = new CageType { Name = "Small lion cage" };
var location = new Location { Name = "House of Africa" };
var cage = new Cage(cageType, location);
session.Save(cageType);
session.Save(location);
session.Save(cage);
session.Flush();
session.Clear();
var fromDb = session.Get<Cage>(cage.Id);
Assert.That(fromDb.CageHistories.Count(), Is.EqualTo(1));
}
[Test]
public void changing_location_of_a_cage_creates_a_cage_history()
{
var cageType = new CageType { Name = "Small lion cage" };
var location = new Location { Name = "House of Africa" };
var location2 = new Location { Name = "House of Madagaskar" };
var cage = new Cage(cageType, location);
session.Save(cageType);
session.Save(location);
session.Save(location2);
session.Save(cage);
session.Flush();
session.Clear();
var fromDb = session.Get<Cage>(cage.Id);
fromDb.ChangeLocation(location2);
session.Flush();
session.Clear();
var fromDb2 = session.Get<Cage>(cage.Id);
Assert.That(fromDb2.CageHistories.Count(), Is.EqualTo(2));
Assert.That(fromDb2.Version, Is.EqualTo(2));
}
}</p>
The two tests above should be pretty much self describing.
Summary
In this post I have shown how history of an entity can be made explicit by making it a domain concept. This approach is much more discoverable than a implicit approach where history is generated auto magically by e.g. using an interceptor mechanism at the database or ORM framework level.
In a following post I will introduce animals to populate the cages and discuss the implications to the model and mapping of the model.