Turn finders into associations and get caching for free

Let’s say we have a method on a model that looks something like this:

def last_assignment
  assignments.find(:first, : order => 'created_at DESC')
end

We call this method several dozen times from various other methods on the model. The problem is, every time we call the method a new database query is triggered. This happens even if we make multiple calls within the same method. For example:

def last_assignment_is_old?
  last_assignment && last_assignment.completed_at && last_assignment.completed_at < 30.days.ago
end

This results in three identical queries to the database. How wasteful! Let's fix it:

def last_assignment
  @last_assignment ||= assignments.find(:first, : order => "created_at DESC")
end

Ahhhh, this is much better. Now we're storing the result as an instance variable on the model. Our last_assignment_is_old? method will only trigger a single call to the database now since we're caching the result. But what happens if we do something like this?

puts model.last_assignment.nil?
model.assignments.clear
puts model.last_assignment.nil?

On the console, we should see false and then true, right? What we actually see is false and false. Even though we clear out the assignments from the model, the last assignment is still stored in the @last_assignment instance variable. Since we're dealing with the same instance of the model, the cached value is returned. In this particular case, that's not good!

The way to get around this is by using an association:

has_one :last_assignment, :class_name => "Assignment", : order => "created_at DESC"

This association basically says, "Sort the assignments by the creation date and give me the first one in the list." After running our test code again, we're golden. It turns out that Rails' associations provide caching automatically. All we have to do is remember to call reload on our model before querying it again. This will ensure that we're working with fresh data:

puts model.last_assignment.nil?
model.assignments.clear
puts model.reload.last_assignment.nil?

Now we get false and then true as expected. The reload trick won't work with instance variable caching, unfortunately, which is why the association is a better choice in this case.

After examining a few more of our models, we discover that there are many such finder methods that can be converted to associations. We quickly begin converting them, hoping nobody notices that we've been senselessly operating without freebie caching for so long.