Getting Things Done with Active Record

July 07, 2005

Active Record is the object-relational mapping (ORM) layer that comes out-of-the-box with Rails. With zero coding and configuration you can create, read, update, and delete database rows all from the comfort of your very own domain models. But the story doesn't end there, nor should it. When you need to go beyond basic CRUD—to create complex queries, adapt to a legacy schema, or otherwise do advanced stuff with your persistent data—Active Record gently hands over the controls. Herewith, I offer two examples from this week's project adventures.

acts_as_nested_set

As this project is a storefront, we sell products. Products live in categories, where categories may have sub-categories of products, and so on. It's products all the way down, and they live in a relational database. In Rails-speak we say products are Active Record models. And with just a little Active Record code (and by that I mean one line), you can manage hierarchical data such as a tree of product categories.

That one line of code is either acts_as_list, acts_as_tree, or acts_as_nested_set. Those declarations make an Active Record model act as a list, tree, or nested set, respectively. Dave expertly walks you through the first two declarations in Agile Web Development with Rails. He saved acts_as_nested_set as an exercise for the reader. Coincidentally, I got an opportunity to learn it first-hand on our project, where it was already being used.

It turns out acts_as_nested_set is similar to acts_as_tree, but it adds a clever twist: you can gather entire sections of the tree with a single query. This concept isn't new or unique to Rails. Indeed, it has been implemented in several languages for our forefathers. In fact, the article that really helped me wrap my head around nested sets (see Storing Hierarchical Data in a Database) uses PHP in its examples.

So this isn't novel stuff, but Rails does make it quite easy to implement. Provided your products table has parent_id, lft, and rgt columns, here's your Active Record model:

class Product < ActiveRecord::Base
  acts_as_nested_set
end

If your products table doesn't use the column names expected by Active Record for acts_as_nested_set, you'll have to write a wee bit more code to override the defaults.

class Product < ActiveRecord::Base
  acts_as_nested_set :parent_column => "your_parent_id", 
                     :left_column => "your_left", 
                     :right_column => "your_other_left"
end

In any event, now you can get all the children of a given product using the object-oriented interface.

product = Product.find(123)
child_products = product.all_children

And behind the scenes, all the products in that section of the tree are rolled up with a single SQL SELECT:

SELECT * FROM products WHERE (lft > 743) and (rgt < 34583)

Being able to do that in one query is like gold when you're working with mass quantities of products organized in arbitrarily deep categories. The solution is clever, and first-class Active Record support for hierarchical data means you can spend more time on other things.

find_by_sql

Being an object-relational mapping layer, Active Record lets you deal mostly with objects. To find a persistent object (an Active Record model) you can use various and sundry finder methods. Then depending on how you declare the relationships between models—has_one, has_many, or has_and_belongs_to_many—Active Records add methods to the models to help you conveniently navigate through objects. Here are just a few examples:

class Company < ActiveRecord::Base
  has_many :customers
end

company = Company.find_by_name("Acme")
company.customers.each do |customer|
  puts customer.name
end

new_customer = Customer.create(:name => "Fred", :status => "Gold")
company.customers << new_customer
company.save

gold_customers = Customer.find(:all, 
                               :conditions => "status = 'Gold'", 
                               :order => "name, credit_limit DESC")

Working with objects is effortless and enjoyable, so you end up using them most of the time. But once in a while you have a complex or performance-critical query. In those cases the object-relational impedance mismatch can become amplified. Rather than holding you back from the power of SQL, Active Record kindly steps aside to let you get your work done.

customers = Customer.find_by_sql("insert arbitrarily complex SQL here")

Some may think this breaks object purity. I think it's just productive. After all, I know there's a database under there, and SQL is incredibly powerful for querying relational data. Most of the time I'd rather not write SQL because I'm doing CRUD operations. In those cases, Active Record intervenes to boost my productivity. And when SQL is the best tool for the job, Active Record gets out of the way to let me be most productive.

Now back to object purity. Models manage the state of your application, and Active Record models specifically deal with storing state in a relational database. That is, models encapsulate how objects are swizzled from database rows. You don't have to add specials methods to your models to make this happen, by default. But when you do reach for find_by_sql, it's wise to encapsulate the custom query in a method of a model.

class Customer < ActiveRecord::Base
  def find_complex_relationship
    Customer.find_by_sql("insert arbitrarily complex SQL here")
  end
end

related_customers = customer.find_complex_relationship

Encapsulating queries this way makes your life easier down the road. When you want to add logging, measure performance, or apply some fantastic optimization, you only have to change code in one place.

What I've Learned

Active Record isn't the first ORM layer I've used, nor will it be my last. What I've found unique about Active Record is how well it strikes a balance between letting me deal with objects when it's convenient and giving me control when I need it. In other words, Active Record is all about getting things done by using the right tools for the job.

But don't take my word for it. Give Active Record a spin for yourself. These examples barely scratch the surface of Active Record. It's certainly no toy. Dave wrote two beefy chapters on Active Record alone in the book. I'd encourage you to give those a read if these examples have left you wanting more.

Read more posts in the blog archive »