ActiveRecord has (bad!) consequences.

This started out as a long comment on Brian Kotek's recent post about the ActiveRecord pattern. I figure that while I've still got some Rails-ish types irked at me, I may as well go ahead and blog a bit more about why I don't like the datacentricness of the ActiveRecord approach.

Before you fire up your flame generation script, let me say this: ActiveRecord is super-productive when you need to allow someone to edit a relational model. It doesn't, however, suit the kind of OOD-backed development I do. This post is about why.

An ActiveRecord implementation, according to Martin Fowler, should have properties that "exactly match that of the database: one field in the class for each column in the table."

Ok, that's straight-up simple.

Let's take this thought further, applying rampant use of the "convention" principles of Rails'. If you take a look at the RoR Wiki, you'll see that there are a number of "magic field names" that can be added to a database/AR model to automagically populate data and even add behavior (such as tree-like abilities via a magic name and an acts_as_wibbleWobble statment).

Ok, that makes it really easy to do something like update "created" and "updated" dates on insert/update.

From a Rails perspective, here's what it'd look like to have a Contact that's both acting as a tree (via adjacency list, aka "parent-child") and using the created/updated_on bits:

Ok, still simple. It makes sense.

It breaks down, miserably, when spread across a large object model, especially when changes occur. Let's pretend you're working in a good-sized CRM implementation with 40-50 domain objects that get persisted with audit information, and 3-4 trees. That's a lot like my main project right now (except it isn't a CRM system).

Problem Scenario One

Pointy-haired boss comes in and says "That audit information isn't enough. Storing when something was changed isn't what I need - I need to know who changes it."

No problem. You go to the Rails wiki and find out this is common. So common, in fact, that code is provided to do it.

You do what it says, then add the created_by and updated_by columns to your 40-50 tables (no way to avoid that). Rails'll pick up the changes and update your....40 or 50 domain objects?

Wait, why should one simple request to change one aspect of behavior impact this many domain objects?

That should set off a warning sign about ActiveRecord. A big red sign.

From the perspective of OO design (not data modeling, which is what ActiveRecord pushes), we're really interesting in spreading a common set of audit information across a domain. In fact, it'd be better to express it as this, moving created_by and such out of our Contact:

Ok, that's better. Now, audit information is modeled as AuditInformation, and any class that needs audit behavior implements IAuditable.

But what about that mega-easy convention stuff to populate it?

It just got easier. Now, instead of checking for the presence of a field (sloppy!)...

model.find(self[:created_by])

...we can use that wonderful thing called polymorphism to see if we're dealing with an IAuditable:

if (model is IAuditable) {
// populate model.auditInformation with whatever we want }

That's a lot cleaner: one change (such as adding who changed something) changes one class, and because we use OO concepts instead of relational concepts, we get all-around cleaner.

Problem Scenario Two

The first problem was kind of elementary. Solving trees is a more complicated task.

Rails does, to its credit, make it fairly easy to create adjacency and nested set trees.

However, if we wanted to change our tree-based objects from adjacency to nested set, we'd be in the same trouble as before: we'd change all our domain objects implementations because we swallowed the Rails kool-aid and repeatedly spread the same behavior across multiple domain objects instead of drinking the OOD brew of isolating what changes, identifying interfaces, and encapsulating behavior.

Here's how I'd approach a system that may change between tree behaviors:

Again, we isolate what may change (the approach to tree implementation), determine a contract (interface), then use composition to give us a flexible, OO domain model, rather than an elementary data model reflected into an API.

(Side note: this is a vanilla implementation of the Strategy Pattern.)

Consequences of ActiveRecord

Yes, ActiveRecord has consequences. Followed blindly, we've seen that it leads to poor OO design by favoring data model reflection over interfaces, encapsulation, and all that good OO jazz. I'm not saying there aren't ways around it (I'm sure John Paul Ashenfelter is already writing his comment ;). [Hi John, buy you a beer at MAX?]), but does it seem realistic that most people using ActiveRecord are going to stop, think about what they're doing, and refactor?

Going out on an editorial limb, it's crossed my mind that one of the causes of the Java -> Ruby (on Rails) shift might be that many Java "developers" didn't get OOP or OOD to begin with and RoR lets them change to something that helps them build CRUD apps by providing massive relational crutches. While these crutches could be useful in good hands (I have a hard time believing that companies like Fowler's ThoughtWorks use Rails inappropriately!), are they just helping others limp along and avoid good design?

Ok, better stop before I go full rant mode.

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Great post Joe! I think you're probably right on with that editorial note - I've often found myself thinking it would be easier to embrace those "massive relational crutches" instead of learning good OO principles and design patterns.
# Posted By Aaron Roberson | 8/21/07 4:10 PM
Joe, you're a funny guy. Glad I read all the way to the end to see that you anticipated the post :)

There's *always* ways to work around design and implementation concerns, regardless of the language and I find that a lot of good CFers sometimes need a bit more Rails and Ruby under the belt to really let go of the preconceptions. I get it though, all of us CFers have had to deal with the "its a toy, it's not Java, blah blah" years :)

Anyhow, Joe's Problem Scenario One is a common one, and just because Rails ActiveRecord has some magic fields does NOT mean you should blindly implement it using such a brute-force method anymore than you'd implement it that way using Transfer or Hibernate or any other ORM. The problem here is quite correctly one of encapsulating common behavior, factoring it out, and implementing the interface, or in this case, a Ruby mixin. Joe's neatly solved the problem for us with that great UML diagram of the relationship. I'll supply the code:

class AuditObserver < ActiveRecord::Observer
   observe Object1, Object2, .... Object40, .. Object50
   
   def after_save(model)
      // write data to a common audit table, log it, etc
   end
   
end

Actually, thanks to Ruby (not Rails, Ruby) mixin magic, you don't even have to modify the 1-50 domain objects. Ironically, as the code implies, this is built-into ActiveRecord, and to be blunt, discussed in the Agile Web Development with Rails book (2nd Ed, p 377-378) which is the same one that pushes database-driven development in the intro chapters.

The second example could be approached a few ways. An Agilista might say "you're not going to need it" and wait until the strategy DOES change before implementing the strategy pattern. A pragmatic programmer might simply do a search/replace for acts_as_tree and replace it with acts_as_list which mixes in the appropriate strategy and then use the migrations facility for ActiveRecord objects in Rails to write a script to change the database, which requires either adding or removing the position field (list uses it, tree just needs a parent).

I think the problem most folks new to Ruby have is with mixins and how they are both similar and different to interfaces, plus the whole idea that classes are easily reopened for change and metaprogramming on the fly. I find that mixins encapsulate a ton of functionality that drops directly into the model object, to add the appropriate domain behavior to the object that maps directly onto the database using the ActiveRecord pattern. I don't really see what I'm giving up as far as isolating changes, encapsulating behavior, and identifying interfaces... other than the fact that they can all be changed at any time :)
# Posted By John Paul Ashenfelter | 8/21/07 4:48 PM
Hi Joe: +1 to what John Paul wrote. I was going to mention the same things, specifically mixins to take care of your interface issue and YAGNI about switching trees (search and replace can be your friend too).

For the first issue, there's no point in choosing the design that changes every table. Instead, create a new table that references records in the other tables. Why duplicate the same thing everywhere?
# Posted By Sammy Larbi | 8/21/07 5:47 PM
Joe,
I get that ActiveRecord marries the object design to the relational schema and where that can be a problem. In the context, of CF development, though, to me that pales into insignificance beside the practice of slinging around raw recordsets (yes, I'm still not over it). At least with ActiveRecord the object model exists. So when you're advocating clean OOD (hear, hear!), do you mean all the time, or are you still allowing for dropping back into recordset mode when needed? Or do you think there's no conflict there?
# Posted By Jaime Metcher | 8/21/07 6:00 PM
I think folks are missing Joe's point to some extent. He's not saying that Rails (and other ORM-based systems) *can't* support good OO practice, he's saying that mainstream common usage leads to badly designed systems. It's the same argument that can, rightly, be applied to any system that starts with the database and slavishly replicates it up to the object model and service layer.

We see the same thing in ColdFusion with what I call the "5:1 Syndrome" where you get five CFCs for every table in your database: bean, DAO, gateway, manager/service and controller.

That's just poor OO design - it's really just relational design mapped into objects, instead of a proper top-down OO design.

Matt Woodward even acknowledged this with his preso on application architecture "back to front" (at cf.Objective() and CFUNITED).

The problem with ORM technologies in general is that it encourages propagation of relational models up into the OO world instead of designing a good OO model and mapping it down to the relational world.
# Posted By Sean Corfield | 8/22/07 4:01 AM
Sean, you mentioned "he's saying that mainstream common usage leads to badly designed systems."

I don't think its fair to put that on DB-design-first or ActiveRecord or anything specifically. That's a general problem in software development, I think.
# Posted By Sammy Larbi | 8/22/07 7:52 AM
With all due respect to Sean, I think it's really clear that what Joe was saying was

"I don't like the datacentricness of the ActiveRecord approach"

That pretty much sums it up clearly :)

Taking that dislike of a pattern and then moving on to critique an *implementation* of that pattern is where we're starting to get into toe-stepping-upon. Active Record *is* a very datacentric approach, but frankly the implementation of ActiveRecord (note the difference 'Active Record' is a pattern, 'ActiveRecord' is a Ruby library) used in Rails really provides a domain-specific language for describing arbitrary ORMs, which in a lot of ways is *far* more than 1 object -> 1 table of vanilla Active Record pattern. Database platform details are abstracted out, SQL is abstracted out, relationships are modeled, validation and other business logic is hooked in, there is a series of callbacks for injecting new code behavior and tons more built into 'ActiveRecord', which goes quite a bit beyond Fowler's pattern. What's really wrong is the name of the Ruby library.

Sean's main point is quite important, though seems different than Joe's -- in that "slavishly" (good and accurate word) mapping database tables -> objects and calling that an OO approach is probably not a very rich model except for the most basic cases. Every time someone tells me I need 5 CFCs to model a single object because "that's the right way to do it OO" it makes me sad, sad, sad.

What Joe's done a great job of (so I'll buy YOU a beer back) is show two really common examples of what's good about having objects and how good design can handle the need for change without requiring you to always rewrite the objects. Of course there's even more solutions to the problems he posed -- aspects for example are a fine choice for logging/auditing without the objects even knowing about the process.

So I think to be fair Joe and Sean now need to write a diatribe about the "5:1 syndrome" which is what I'll happily start calling that CF 'pattern'.
# Posted By John Paul Ashenfelter | 8/22/07 10:31 AM
Boy, am I late to this thread! Mea culpas aside, I agree fully with Joe and Sean. I love Ruby but don't love Rails because of the reliance on the ActiveRecord pattern. Joe's right: it encourages sloppy practice.

Relational Entity/Relationship schemas have a wonderful logic -- for data storage. Object Modeling has its own logic and sacrificing that so that we can get some code pre-written for us is just a bad idea.
# Posted By Hal Helms | 2/18/08 2:23 PM
© 2008 Firemoss, LLC
BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.