Where to from here? Moving on from ActiveRecord…

Quite a while back I posted an entry about rejoining the pack, and it’s been pretty common since for me to face the question “how’s it going” and “why did you move”. Well, this post is a well overdue response about the decisions I’m following now. This particular post focuses on my move from ActiveRecord, however I’m planning to write similar posts for the other areas discussed in my original post.


I hate datasets. Adam Cogan once said in a presentation, “if you like fat women, you’ll like datasets”. Even that statement doesn’t go far enough to describe how much I don’t like datasets. But that’s another story… For this post, lets just assume that you’re sold on the concept of domain objects.

ActiveRecord gave me a quick and relatively simple way of filling my application with what I then thought were nice domain objects. I didn’t need to write any SQL. I could place validation on a property using a single attribute like [ValidateNotEmpty] or even write custom ones like [ValidateAustralianBusinessNumber]. Life was good.

In then end, I built two major sites using ActiveRecord, and they are still using their ActiveRecord implementations today. Via Virtual Earth serves 5,000 visitors an average day and 2,000,000 on a big day (conferences or product launches). Male Order Only is a fully fledge e-commerce site that’s starting to get some heavy press in international fashion circles.

Unfortunately, ActiveRecord then started to show it’s faults. One of my biggest motivations to move was code flexibility – with AR I almost feel backed into a corner when it comes time to do maintenance on these sites. The other major motivation was performance – ActiveRecord sits on top of NHibernate which sits on top of ADO.NET, and there’s a ton of reflection mixed in along the way. With a framework that deep, performance is always going to be an up-hill battle and currently it’s a battle that’s hurting us.

To be fair, I’m not an AR guru and some of these issues are probably completely my fault. On the same note however, there are a number of use cases I have since come to discover that AR just doesn’t handle nicely.

Where to from here? Much of where I’ve ended up today in the realm of domain models is derived from Paul Stovell‘s Trial Balance project. I think he’s finally come to the realization that he will never finish the product, but in the seven or so re-writes so far he’s had the opportunity to try a lot of different techniques. I like where he’s ended up, and this is the architecture I’ve started implementing for my latest round of projects. Considering Hammett lives in Brazil, and Paul lives 5m away in the other room, Paul probably had a considerably easier job of convincing me, but consider me convinced.

Lets take a look at some code…

The whole domain model (entities and data providers) sits within FuelAdvance.<ProjectName>.DomainModel.

Each of my entities have their own class that inherits from DomainObject (I’ve trimmed a number of the properties to simplify the sample):

namespace FuelAdvance.<ProjectName>.DomainModel.Entities
{
    public class Voucher : DomainObject
    {
        private Guid _voucherId;
        private string _toName;
        private string _fromName;
        
        internal Voucher(IDataProvider dataProvider)
            : base(dataProvider)
        {
            _voucherId = Guid.NewGuid();
        }

        [DataProperty("Voucher Id")]
        public Guid VoucherId
        {
            get { return _voucherId; }
            set
            {
                AssertCanEdit();

                if (_voucherId != value)
                {
                    _voucherId = value;
                    NotifyChanged("VoucherId");
                }
            }
        }

        [DataProperty("To Name")]
        public string ToName
        {
            get { return _toName; }
            set
            {
                AssertCanEdit();

                if (_toName != value)
                {
                    _toName = value;
                    NotifyChanged("ToName");
                }
            }
        }

        [DataProperty("From Name")]
        public string FromName
        {
            get { return _fromName; }
            set
            {
                AssertCanEdit();

                if (_fromName != value)
                {
                    _fromName = value;
                    NotifyChanged("FromName");
                }
            }
        }   
        
        public override void CopyTo(object targetObject)
        {
            base.CopyTo(targetObject);

            Voucher targetVoucher = targetObject as Voucher;
            if (targetVoucher != null)
            {
                targetVoucher.VoucherId = this.VoucherId;
                targetVoucher.ToName = this.ToName;
                targetVoucher.FromName = this.FromName;
            }
        }
    }
}

There are a few things to note here:

  • The entity has an internal constructor, and it keeps a reference to the data provider that created it.
  • The propeties seem a bit wordy, but if you take a close look you’ll actually see they’re doing quite a bit such as asserting whether the object is read-only, and providing INotifyPropertyChanged notifications. This is helped with a simple snippet:

 

  • Each property is decorated with a custom DataProperty attribute. This is entirely optional – but I’ll explain it more later.

The entity inherits from an abstract DomainObject class within the same project. All of the underlying logic actually sits in the very simple FuelAdvance.Components.Modeling.DomainObject<T>, however we need this interim class to keep some of the properties internal to our domain model.

namespace FuelAdvance.<ProjectName>.DomainModel
{
    public abstract class DomainObject : DomainObject<IDataProvider>
    {
        internal DomainObject(IDataProvider dataProvider)
            : base(dataProvider)
        {
        }

        internal new IDataProvider DataProvider
        {
            get { return base.DataProvider; }
        }

        public new bool IsReadOnly
        {
            get { return base.IsReadOnly; }
            internal set { base.IsReadOnly = value; }
        }
    }
}

All pretty simple so far right? Now, lets take a look at how we interact with these objects as a consumer from say FuelAdvance.<ProjectName>.WebUI.

Retrieving an object is pretty straight forward – we just call the data provider:

IDataProvider dataProvider = new SqlDataProvider(connectionString);
Voucher voucher = dataProvider.GetVoucher(voucherId);

By default though, our object is read only so this would throw an exception (remember that AssetCanEdit earlier?):

voucher.ToName = "Tatham Oddie";

This is where the ChangeCoordinator steps in. The change coordinator maintains a link to the original item, as well as a working copy where we perform our edits. It is the data provider’s responsibilty to provide us with the change coordinators. An edit might look like this:

ChangeCoordinator<Voucher, IDataProvider> changeCoordinator = dataProvider.AcquireChangeCoordinator(voucher);
changeCoordinator.WorkingCopy.ToName = "Tatham Oddie";
changeCoordinator.PushChanges();

The PushChanges() method is responsible for pushing the changes from the working copy to the original item, and then firing a ChangesPushed event. When the data provider made our change coordinator, it subscribed to this event so that it could persist the changes at this time.

The actual process of pushing the changes from the working copy to the original item is achieved using the CopyTo() method on our entity.

PushChanges() can only be called once – after this time the working copy is read only, and any subsequent calls to PushChanges() will fire an exception.

There are lots of advantges to this approach:

  • In a winforms secnario we might have a list of records. When we double click a record in the grid, a form is opened to edit the individual record. If the user makes a change and clicks Ok, we expect this change to be visible in the original listing, but no earlier than this. There are a number of approaches here:
    1. We pass the voucher ID to the detail form. The detail form loads it’s own instance of the object. When the user clicks Ok, the detail form persists the changes then notifies the original form there has been a change. The original form reloads its list. There’s lots of reloading going on here…
    2. We pass an instance of the voucher to the details form. The detail form uses data binding. When a field is changed in the detail form, that change is instantly visible in the list (not good as they haven’t clicked ok yet). To support the cancel button, we keep a copy of the original values somewhere.
    3. We pass an instance of the voucher to the details form. We manually populate the fields from the instance (no databinding). When the user clicks Ok we manually copy the values back.
    4. We use a change coordinator. The details form binds to changeCoordinator.WorkingCopy. When the user clicks the Ok button, we call changeCoordinator.PushChanges(). When the original object gets updated the INotifyPropertyChanged interface means the original list gets immediately updated without having to re-poll the database. The if user clicked Cancel, we jsut abandon the change coordinator.
  • As part of the PushChanges event we actually receive a List<Change> where each change includes the property name (eg. “FromName”), a friendly name retrieved from the DataProperty attribute (eg. “From Name”), the original value (eg. “Tom Harvey”) and the new value (eg. “Tatham Oddie”). Only properties that actually changed are included in this list. We can use this list for a number of things:
    1. Implementing auditing. (I have some really cool stuff in this space that I will cover in a later post)
    2. Complex security policies, for example: “The ‘from name’ on a voucher can only change if the voucher has not yet been claimed or expired.”
    3. Complex validation, for example: “If the expiry date is changed, it must be equal to or greater than it’s original value.”
  • We can persist changes without having to go back to the original data provider and call something like Save(voucher), and we don’t need need to make the entity aware of how to persist itself either.

We use this same approach to create new instances as well:

ChangeCoordinator<Voucher, IDataProvider> changeCoordinator = dataProvider.CreateVoucher();
changeCoordinator.WorkingCopy.FromName = "Tatham Oddie";
changeCoordinator.WorkingCopy.ToName = "Tom Harvey";
changeCoordinator.PushChanges();

In this case, changeCoordinator.OriginalItem is null. When the data provider receives the ChangesPushed event it can easily detect this case and act accordingly.

Alright, so lets take a look at what’s under the covers of one of these data providers then!

To start off with, we define an interface for our providers:

namespace FuelAdvance.<ProjectName>.DomainModel
{
    public interface IDataProvider
    {
        Voucher GetVoucher(Guid voucherId);
        ChangeCoordinator<Voucher, IDataProvider> CreateVoucher();
        ChangeCoordinator<Voucher, IDataProvider> AcquireChangeCoordinator(Voucher voucher);
    }
}

When defining your data provider interface, it’s crucial that you only create the methods you need. It’s very easy to say “well we need a GetAll, a Get, a Save, a Create, a …”. No you don’t! Start with nothing, and add the methods as you need them. You’ll also notice at this point that our data provider interface doesn’t have any save methods. This is because the concrete implementation is responsible for providing the change coordinators, and thus provides it’s own persistence logic at this point.

For our example, the SqlDataProvider implementation might look like this:

namespace FuelAdvance.<ProjectName>.DomainModel
{
    public class SqlDataProvider : IDataProvider
    {
        private StoredProcedureHelper Helper;

        public SqlDataProvider(string connectionString)
        {
            this.Helper = new StoredProcedureHelper(connectionString);
        }

        public ChangeCoordinator<Voucher, IDataProvider> CreateVoucher()
        {
            return CreateChangeCoordinator<Voucher>(null, new Voucher(this));
        }

        private void CreateVoucher(Voucher voucher)
        {
            Helper.ExecuteNonQuery(
                 Helper.CreateCommand("[dbo].[CreateVoucher]",
                      Helper.CreateParameter("@VoucherId", voucher.VoucherId),
                      Helper.CreateParameter("@FromName", voucher.FromName),
                      Helper.CreateParameter("@ToName", voucher.ToName)));
        }

        private void SaveVoucher(Voucher voucher)
        {
            Helper.ExecuteNonQuery(
                 Helper.CreateCommand("[dbo].[SaveVoucher]",
                      Helper.CreateParameter("@VoucherId", voucher.VoucherId),
                      Helper.CreateParameter("@FromName", voucher.FromName),
                      Helper.CreateParameter("@ToName", voucher.ToName)));
        }

        public Voucher GetVoucher(Guid voucherId)
        {
            return Helper.ExecuteSingleSelectCommand<Voucher>(
                 Helper.CreateCommand("[dbo].[GetVoucher]",
                      Helper.CreateParameter("@VoucherId", voucherId)),
                      delegate()
                      {
                          Voucher voucher = new Voucher(this);
                          voucher.IsReadOnly = false;
                          return voucher;
                      },
                      GetDefaultSealObjectInstanceDelegate<Voucher>());
        }

        #region Change Coordinators

        public ChangeCoordinator<Voucher, IDataProvider> AcquireChangeCoordinator(Voucher voucher)
        {
            return CreateChangeCoordinator<Voucher>(voucher, new Voucher(this));
        }

        protected ChangeCoordinator<T, IDataProvider> CreateChangeCoordinator<T>(T originalItem, T workingCopy) where T : DomainObject
        {
            if (originalItem != null && originalItem.DataProvider != this) throw ExceptionHelpers.NewInconsistentDataProviderException();
            if (workingCopy == null) throw ExceptionHelpers.NewArgumentNullException("workingCopy");

            ChangeCoordinator<T, IDataProvider> coordinator = new ChangeCoordinator<T, IDataProvider>(originalItem, workingCopy);

            coordinator.ChangesPushing += new ChangeCoordinatorChangesPushingEventHandler<T, IDataProvider>(ChangeCoordinator_ChangesPushing<T>);
            coordinator.ChangesPushed += new ChangeCoordinatorChangesPushedEventHandler<T, IDataProvider>(ChangeCoordinator_ChangesPushed<T>);

            return coordinator;
        }

        private void ChangeCoordinator_ChangesPushing<T>(object sender, ChangeCoordinatorChangesPushingEventArgs<T, IDataProvider> e) where T : DomainObject
        {
        }

        private void ChangeCoordinator_ChangesPushed<T>(object sender, ChangeCoordinatorChangesPushedEventArgs<T, IDataProvider> e) where T : DomainObject
        {
            if (typeof(T) == typeof(Voucher))
            {
                if (e.ChangeCoordinator.OriginalItem == null) CreateVoucher(e.ChangeCoordinator.WorkingCopy as Voucher);
                else SaveVoucher(e.ChangeCoordinator.WorkingCopy as Voucher);
            }
            else throw ExceptionHelpers.NewNotSupportedException();
        }

        #endregion

        #region Stored Procedure Helpers

        private StoredProcedureHelper.SealObjectInstanceDelegate<T> GetDefaultSealObjectInstanceDelegate<T>() where T : DomainObject
        {
            StoredProcedureHelper.SealObjectInstanceDelegate<T> result = delegate(T target)
            {
                target.IsReadOnly = true;
            };

            return result;
        }

        #endregion
    }
}

A few things to note:

  • Most of the code there consists of helper methods useds to create our change coordinators
  • We’re using stored procedures to load everything
    • Much more effecient than the dynamic SQL that automated ORM systems like to use
    • Lets a DBA go an restructure as much as they want without affecting the program (while most developers won’t admit it, they really do suck at SQL and thus should be leaving the opportunity there for a DBA to clean it up later)
  • The CreateVoucher(Voucher) and SaveVoucher(Voucher) methods are both private to the data provider implementation – our consumers don’t need to use these
  • In this case we had separate methods for creating and saving vouchers, however we could have very easily used the same method – this is a case that many systems don’t support very well. It doesn’t always have to be CRUD you know …
  • The SQL code is very succinct thanks to the StoredProcedureHelper class.

The more I think about it, I just don’t see the value that a mammoth framework like Active Record or NHibernate offers. Sure, I have an underlying component library (FuelAdvance.Components.Modeling) but that’s a total of 8 classes with maybe 500 lines of code. The power is in the architecture, not the underlying codebase.

I’d like to make some more posts about this and cover things like relationships and security. Let me know which areas interest you most.


You can download the latest version of the code related to this post from our SVN repository here:

If you find a bug, please email me, or better yet, email me the patch.

4 thoughts on “Where to from here? Moving on from ActiveRecord…

  1. While AR takes away some flexibility in favor of making the common paths easy, I do not agree that it is hard to maintain.
    That said, I am a member in both Active Record and NHibernate teams, so I am biased.

    What were the issue that you have run to?
    I should probably mention that NHibernate is doing quite a bit to avoid the cost of reflection. Some of the things that we do include runtime code generation to avoid reflection ( so there is one time cost, instead of ongoing one)

    I also disagree about NH making it hard for the DBA to optimize. There are well defined extension points for custom SQL, and NHibernate was built to know how DB act, and to take advantage on them.

  2. It’s funny, we are looking at moving away from a similiar approach (to the data provider model) and to Active Record.

    We are sick of having to maintain stored procs and the related dataproviders every time we make changes or additions. We are an extremely high volume application from a data perspective so we will need to do performance testing. But we figure the worst case scenario is we go back to the data providers for specific use cases where the ORM is hurting us (if it does).

Comments are closed.