Keep eager loaded associations in association extensions

Association extensions are amazingly powerful. If you're not familiar with them, here's a small example of their power:

class Person < ActiveRecord::Base
  has_many :items do
    def expensive(expensive_price = 50.00)
      @expensive ||= find(:all, :conditions => [ "price >= ?", expensive_price ])
    end
  end
end

Then, you might do something like this from your view:

<h1>Adam's Expensive Items</h1>
<% for item in @person.items.expensive %>
  <span class="item-name"><%= h item.name %></span>
  <span class="item-price"><%= number_to_currency item.price %></span>
<% end %>

What Rails is actually doing when the find method is called in the above example is running the find within the scope of its parent association, Person. It effectively gives you the following SQL call:

SELECT *
  FROM people, items
 WHERE people.id        = items.person_id
   AND items.person_id  = 1
   AND items.price     >= 50.00

Now, let's assume you've got the following code in your controller that makes use of eager loading:

class PersonController < ApplicationController
  def show
    @person = Person.find(params[:id], :include => :items)
  end
end

You've included the items association, but in your view, you're still calling the find method in your assocation extension, which makes your eager loaded association less effective. Fortunately, Rails handles association extensions in a smart way that makes sure it plays nicely with eager loading, if you want it to. Here's how:

class Person < ActiveRecord::Base
  has_many :items do
    def expensive(expensive_price = 50.00)
      @expensive ||= self.select { |item| item.price >= expensive_price }
    end
  end
end

The above code makes use of the eager loaded association in self, if available, with the select method invoked, which is basically the same as a find(:all …) call. If the association has not been eager loaded, Rails will load issue the SQL to load it, then will run the select method against the returned collection.

One thing to note about this approach, however, is that the call to self.select will still fetch all the results of the association (that is, every item for the particular person), whereas going the route of using the call to find will only pull those records which match the conditions. If you have hundreds or thousands of items for each person, the latter method might not scale to your needs and sacrificing one more SQL call, as demonstrated in the former, would probably be the better option.

Leave a Reply