O/R Mappers: Optimistic Concurrency
I finally got around to adding support for optimistic concurrency to my WilsonORMapper, and it definitely had me think through quite a few design scenarios that I'll share here. Optimistic concurrency works by adding some portion of the record's original values to the where clause of the update statement to make sure no one else has updated it first. Modifying the update is relatively easy, so my issue was that I needed original values. It was tempting to think I had the original values already, since I do track the values, but the problem is that my broker might be used in a multi-user web or distributed app. This means that the values I'm tracking will always be the last known persisted values, which is great for some scenarios, but this does not support optimistic concurrency.
One thing that should be obvious is that I need to store the original values with the actual object, so I set out to explore the various alternatives to achieve that result. One possible solution is relatively trivial -- just require all entity objects to derive from a specific base class which would include support for storing the original values. This is indeed what many other O/R mappers do, but I had originally chosen to avoid the base class approach for a variety of reasons, and I still have the very same objections.
The next possible alternative is to do the reverse -- instead of having a common base, its possible to create a dynamic assembly where all of the entity types are extended. This actually turned out to be very easy to do also, but this would mean that I would have to give up my current simplicity of allowing the explicit new to create objects. This is one of the cornerstones of my mapper approach -- KISS -- Keep It Simple Stupid. So I also abandoned this approach, although I may use it someday for some other feature.
Another possibility is to dynamically modify the existing type using some code injection. The most common way to do this in .NET is to use Aspect Oriented Programming techniques, but this requires not just a base class, but a very heavy base -- ContextBoundObject. Another injection solution is to use the RAIL framework, which looks very promising, but then I noticed there was another problem that I had been overlooking from the beginning.
That problem, which seems to be overlooked by MS ObjectSpaces and most other O/R mappers, is that optimistic concurrency is not necessarily always based on all original values! Its actually quite common to just use a TimeStamp column in MS Sql, and I had already just added the ability to configure fields like TimeStamp to be read-only for someone. Well its just natural to extend this idea and allow the user the configure each field with a persistType that can be either Persist (the default), ReadOnly, or Concurrent. This solves the read-only cases like TimeStamp immediately, but it has the downside of forcing my users to create and configure two fields for other fields that should take part in the optimistic concurrency check -- one each to hold the new and original values.
This is indeed an extra burden on the user, but it has a couple of redeeming values too. First, it allows the simplicity I've strived for to remain in the non-optimistic cases. Next, it allows the flexibility to define your concurrency fields, which is missing from at least some other systems -- great if you are using TimeStamp or if have you have so many fields that you don't want to pass them all -- which was something else I had just dealt with. Finally, my mapper also suports updates with stored procedures, and I don't really see how I could have continued to allow them without somehow also allowing the user to have configurable parameters for both the new and original values anyhow -- so case closed.
I know that I will probably get derided for this solution, since its not very "pretty", but in the end it meets my goals of simplicity for the main cases, which is my strength, while still offering some additional flexibility if you are willing to configure it.