Me
About
Gallery
Company
Girl


Projects
Ruby on Rails
Basecamp
Highrise
Backpack
Campfire
Ta-da List
Writeboard


More
Feed
Archives

May 27, 17:17

Addressing concerns with Active Record

Aredridel has been one of the beta testers on my Active Record object-relational mapping package that's going to be released in the very near future. It's the model part of Rails and about two thirds of all the code in the framework. So naturally, I'm very interested in feedback, and Aredridel provides just that in his Annoyances with ActiveRecord (currently down, try google cache).

First, I'd like to extend my appreciation of the concerns raised (read them before reading this). Second, allow me to retort.

1. 1:N relationships with N*2 queries

This is exactly why Active Record allows for an intimate relationship with SQL. And why it uses SQL for conditions snippets and more. Active Record really doesn't want you to forget about SQL because SQL is exactly the right answer to a lot of questions. This is one of them. Active Record takes the drudgery out of doing manual SQL for a ton of queries (in Basecamp it's around 85-95% of the queries that are automated).

So when you encounter a case such as this, you just drop down to SQL. If AR gives the intention that SQL is fooling around with the engine that's were the mistake is being made. It's more like choosing manual gears on a car that runs on automatic rather that popping the hood. So I think this is mostly a case of managing expectations. Active Record shouldn't give the expectation that you'll never have to touch SQL again. That's an illusion and many ORMs have been build and crumbled on that illusion.

Taking Active Record to a manual gear on this entails using all the wonderful flexibility put into the ORM through the power that is Ruby. The solution is called "data piggybacks" and works like this:


class Post < ActiveRecord::Base
  def self.find_all_with_authors
    find_by_sql(
      "SELECT posts.*, people.name as author_name " +
      "FROM posts, person " +
      "WHERE posts.author_id = people.id"
    )
  end
end

Now you can do:


for post in Post.find_all_with_authors
  puts "#{post.title} was written by #{post.author_name}"
end

This bends the regular restriction that post objects will only have attributes equal to those in the table definition for that class. Hence the piggyback term.

This is of course a simple example, which you can make more elaborate as your requirements dictate it. But it does show how you can turn n*2 (or n*3 or n*5) type queries back into n. This is mostly important for overview screens that needs to present this data together.

I could imagine some formal support in Active Record for the piggyback approach, but I've found that outside toy examples, like the one above, I really do like to drop to SQL for queries like this. The only reason you're going to SQL is to tweak performance, so it's often you more interested in control than convenience in these cases. But again, I'm certainly open to formal piggyback schemes.


2. ActiveRecord takes over inheritance

It was an early decision that ActiveRecord objects wouldn't be able to live in isolation from the ORM. This follows directly from the Active Record pattern and is one of the main differences between that and the Data Mapper approach. By tying your business objects to Active Record you loose flexibility in swapping in another ORM, but at the same time gain a incredible ease of use and convenience. Active Record objects accepts this dependency by using the association/aggregation macros (has_many, etc), overwriting callbacks (before_save, etc), and implementing validation.

So in my opinion there's little to gain from a mix-in, when you're writing code that formulates a specific dependency anyway. Once you've accepted that, there's no way to "mix-out".

What I'd rather mix-in would be the shared functionality between payment and transaction if you don't want it to use an explicit inheritance hierarchy. That was actually exactly what I did with todo lists and todo items on Basecamp. Both needed functionality to control list movement (move this item/list higher or lower or insert a new item/list). This behavior was kept in a List module that was then mixed into both Active Record classes. Worked very well.


3. ActiveRecord is also very hard to debug

I recognize that especially the association/aggregation macros are pretty hard to debug. But you've yourself remedied that with the module_eval debugger where you get a perfect view into which methods are added by the different macros and what their internals look like.

The other parts of Active Record is more easily debugged by having a look at the logging that ships with the package. All SQL statements generated and executed by Active Record are logged and their run time is reported (poor man's profiler).

The newer versions of Active Record also implements a considerably improved exception system with more saying and relevant exceptions raised.


4. ActiveRecord does not [do a decent job of type guessing] for anything resembling a derivative of a basic type

This just changed with the inclusion of the aggregation module, which brings much needed value object support to Active Record (allowing for a "fine-grained" label). You can now use Active Record objects as aggregations of value objects such as Temperature, Money, Address, and others that doesn't need an identity (such objects are often called entity objects, which is exactly what Active Record-based objects are).

Example (as it will be in the final version):


def Account < ActiveRecord::Base
  composed_of :amount, :class_name => "Money"
end

The amount attribute, which is just represented by an integer in the database, will now be presented as a Money object and can be assigned as such:


account.amount = Money.new(20, "USD")
account.amount.exchange_to("DKK") # returns Money.new(150, "DKK")


5. Take this with a grain of salt. I'm working with a pre-existing application here

Active Record is certainly better suited for new applications than for wrapping around existing structures. The naming, id, and class schemes work much better when you're following the form that Active Record is most comfortable with. So again, trade a bit of flexibility, get ease of use.

It would however not be unreasonable to suggest improvements to Active Record that would allow it to be more easily used for legacy applications. It's just that I've certainly picked sides and tweaked ease of use towards new applications, so suggestions that this awkward probably won't be acccepted.

Thank you, Ari, for bringing these concerns out. This allows me to better formulate what Active Record is and isn't and why.