I still [less than three] ActiveRecord, but the honeymoon is definitely over. Over the past couple of days, I've run into a couple of issues that took some thinking to address. Neither is a deal-killer (not in my opinion, anyway), but they are problems that need to be solved.
ActiveRecord persists changes without you telling it to
Take the following code snippet:
2: int id = 0;
4: using (new SessionScope())
6: Widget w = new Widget();
7: w.Name = "My Widget";
11: id= w.Id;
14: using (new SessionScope())
16: Widget w = Widget.Find(id);
18: Assert.AreEqual("My Widget", w.Name);
19: w.Name = "Changed!";
21: //NOTE: No call to Save here...
24: //This should be in a new SessionScope.
25: Widget w2 = Widget.Find(id);
27: //I would expect Name to be unchanged...
28: Assert.AreNotEqual("Changed!", w2.Name);
The Assert.AreNotEqual actually fails! I never told ActiveRecord to save my changes, but they are saved nonetheless. To understand why, you have to understand what's going on under the hood. ActiveRecord is built on top of NHibernate. With NHibernate, you typically perform data updates in the following fashion: create a session, retrieve your object, make your changes, and flush your session. ActiveRecord is doing the same basic thing. In the above code, it is obviously creating a session and retrieving an object, but it is also flushing the session. That's because when SessionScope is disposed, it flushes changes back to the database. Ok, this make sense, but what about if I have the following (taken from an ASP.NET MVC action):
3: //Assume that there's a session already active
4: Widget w = Widget.GetWidget(id);
6: UpdateModel(w, formValues)
8: //If execution reaches this point, w was updated,
9: //so we'd like to save the record.
12: catch (Exception ex)
14: //If an exception is thrown, we don't want to save the record.
15: //Todo: standard error-handling code...
What happens if UpdateModel is able to update some pieces of Widget, then finds a value that it can't convert and pitches an exception? In this case, Widget shouldn't be updated, but it is!
The solution is (fortunately) simple enough - just use a transaction:
3: using (TransactionScope t = new TransactionScope(OnDispose.Rollback))
5: Widget w = Widget.Find(id);
7: UpdateModel(w, formValues);
12: catch (Exception ex)
14: //Error handling...
If an exception is thrown by UpdateModel, Transaction.VoteCommit is never called, so the transaction will be rolled back. The underlying database will remain unchanged. Sure, it's a bit more work than would be ideal, but it works.
Exposing Foreign Keys as Primitive Types
ActiveRecord has very nice support for all common relationship types, but I found one situation where it simply will not let me do what I want. Say I have a complicated object model that represents a deeply-nested tree with a large branching factor (meaning that the nodes in the tree typically have many children). One simple way to model this would be to use the HasMany and BelongsTo attributes. Each node could have a Parent TreeNode (flagged with BelongsTo) and a ChildNodes collection (flagged with HashMany). This would let me walk the tree from any node by traversing up the tree or down the tree, which is nice. But what if I only want to retrieve a single non-root node for displaying it on a details page? Unless I use lazy-loading, retrieving any individual node will actually retrieve the entire tree. This is wasteful and definitely not what you want.
For more information on ActiveRecord attributes, consult the API reference.
One solution is to remove the Parent reference. This will reduce the severity of the problem above (retrieving a particular node will only cause the subtree rooted at the node to be retrieved), but we lose the ability to determine who the parent is. Ideally, we'd like to know the ID; maybe we want to render a link back to the parent from the child.
Extending the previous solution, you can add a simple property that is mapped to the underlying foreign key column. This actually *almost* works. Anything you retrieve from the database will have the correct value. Unfortunately, the property is not correctly populated when you do a Save on a new node until you reload the node.
The solution I've temporarily settled on is override the node's PostFlush method like so:
1: protected override void PostFlush()
5: //NOTE: ParentID is a nullable int.
6: //If the parent isn't set, try to refresh this instance.
7: if (ParentID == null)
9: using (new SessionScope(FlushAction.Never))
I got the idea from the Dark Side. Basically, you just check to see if the parent ID is null, and if so, trigger a refresh in a new read-only session scope. Doing the refresh in a new read-only scope is important. If you don't do this, you may experience strange behavior and/or exceptions.
As I've mentioned before, you do have to make a few sacrifices if you're going to use ActiveRecord. It isn't as flexible as a hand-rolled data access layer (DAL) in terms of supporting every possible usage, but it allows you to be more flexible by quickly adapting to changing requirements.