metautonomo.us

Confounding URL typists since 2007.

The Cure for ActiveRecord Instantiation Anxiety: Valium

Posted by Ernie on August 24, 2011 at 3:56 pm

No, not the drug — the Ruby gem! Have you ever written code like this?

Model.where(:attribute => 'value').map(&:id).each do |model_id|
  # ...
end

I’m guessing you have, even if only as you were just getting started learning Rails/Ruby. It’s a bad idea.

A Confession

I’ve been guilty of it, a time or two. Normally only if I “know” that the model will never have more than a dozen or two records to instantiate.

Why does code like this make me so anxious? Because instantiating ActiveRecord objects is expensive. It’s expensive in terms of memory, and expensive in terms of CPU cycles. ActiveRecord provides all sorts of syntax-sugary goodness with that memory and those CPU cycles, but when you’re just retrieving a list of values from the database, instantiating AR objects is overkill.

Common Workarounds

As a first step, you might choose to alter your query to limit the values returned by the SELECT:

Model.where(:attribute => 'value').select(:id).map

This helps with memory usage, particularly if your model has a lot of attributes, but you still eat the cost of instantiating the ActiveRecord objects themselves, even if you aren’t loading all of their attributes.

The next step is generally to switch to something like:

class Model < ActiveRecord::Base
  def self.ids_for_conditions(conditions = {})
    query = "select id from models"
    sanitized_conditions = sanitize_sql(conditions) if conditions.present?
    query << " WHERE #{sanitized_conditions}" if sanitized_conditions
    connection.execute(query).map {|r| r['id']}
  end
end

This works well enough, until you find yourself wanting access to another column, especially if now it needs to be typecast or deserialized, or…

Anyway, what (usually) ends up happening is one of two things:

  1. You create a bunch of ad-hoc methods to suit the specific purposes, and promise yourself to go back and refactor later, when you have time.
  2. You just map over collections of ActiveRecord objects, and convince yourself that to do otherwise would be “premature optimization,” anyway.

Both of these solutions suck. Now, maybe you’re much more disciplined than that. If so, good for you! But a casual perusal of the source of a number of existing Rails projects indicates that you, you awesome coder you, are an outlier. And besides, wouldn’t it be better to not have to write that code in the first place?

I think so. So I wrote a tiny little gem (less than 100 LOC) called Valium to do the grunt work for you.

Awesome! Thanks! How does it work?

Why, I’m glad you asked, intrepid reader!

You just include the gem in your Gemfile…

gem 'valium'

…then construct any old query you’d like, and slice off just the attributes you’re interested in using with [].

Model.where(:foo => 'bar', :chunky => 'bacon')[:id].each do |id|
  # ... do something with id ...
end
 
Model.where(:foo => 'bar', :chunky => 'bacon')[:name, :price].each do |name, price|
  # ... do something with name and price ...
end

You get the idea.

Valium. Easy, painless attribute values.

What are you waiting for? Give it a try, or go have a look at the README for more info.

Filed under Blog
Tagged as , , , valium
You can leave a comment, or trackback from your own site.