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.