Rails Gotcha: ActiveRecord Caches Associated Records by Default
Posted: April 23rd, 2009 | Author: gabe | Filed under: Rails | Tags: active record, activerecord, caching, Rails, tips | 2 Comments »ActiveRecord will cache the results of association method calls by default, unless you tell it not to.
(This applies to Rails 2.3.2 and perhaps earlier versions.)
From the documentation:
project.milestones # fetches milestones from the database project.milestones.size # uses the milestone cache project.milestones.empty? # uses the milestone cache project.milestones(true).size # fetches milestones from the database
Normally, this is great. However, if you’re not aware of this default caching, you might see some strange behavior in your app or tests and have no idea what’s going on. This default caching behavior had me and Abel stumped until we read about it in the docs.
Working with the same concept as the example in the documentation, where a Project has many Milestones, here’s a more explicit example of the caching behavior in action:
project_1 = Project.find_by_id(1) project_2 = Project.find_by_id(1) # Load the same Project into two variables project_1.milestones.length # - Hits the db's Milestones table. # - Caches milestones object on project_1. # - Returns 0. Milestone.create(:project_id => 1, :name => 'New Milestone') # Adds a milestone to the project. # But we don't do it through the project_1.milestones association # because that _would_ update project_1.milestones's cached value project_1.milestones.length # Returns 0 (not 1, like you'd expect) # because project_1.milestones was cached when # previously requested above. project_2.milestones.length # Returns 1, because project_2.milestones # hasn't been requested/cached yet. # Note: project_2.milestones is # a COMPLETELY DIFFERENT IN-MEMORY OBJECT # than project_1.milestones. # Taking this point further, here's an explicit example: project_2.milestones << Milestone.create(:name => 'Another Milestone') # Put another milestone on the project through # project_2's milestones association. project_1.milestones.length # Still returns 0, because project_1 already cached it's copy of # the milestones association back when there were 0. project_2.milestones.length # Now returns 2, because only project_2 knows about the new milestone. project_1.milestones(true).length # Returns 2, because ActiveRecord updates the cache # when association(true) is present.
Another funny something to note about the association caching behavior is that even when ActiveRecord uses a cached value, it still emits SQL to the log file. So, don’t let that trip you up either.
Interesting post.. I wrote about this issue recently (http://techspeak.plainlystated.com/2009/03/rails-association-caching-pitfalls.html) but wasn’t aware of the #association(true) option.
Oooh. Thanks for your comment and post too, Patrick. Your post taught me a few things as well.