Structuring a Rails app for Russian Doll Caching with a has_many relationship

caching, fragment-caching, ruby, ruby-on-rails, russian-doll-caching

Solution

One method I did stumble on would be to handle this via the cache keys. Add a `has_many_through` relationship for commenters to the article:

class Article < ActiveRecord::Base
  attr_accessible :author_id, :body, :title
  has_many :comments
  has_many :commenters, through: :comments, source: :author
  belongs_to :author
end

Then in article/show we would construct the cache key like this:

<% cache [@article, @article.commenters, @article.author] do %>
  <h2><%= @article.title %></h2>
  <p>Posted By: <%= @article.author.name %></p>
  <p><%= @article.body %></p>
  <ul><%= render @article.comments %></ul>
<% end %>

The trick is that the cache key generated from the `commenters` association will change whenever a comment is added, deleted, or updated. While this does require extra SQL queries to generate the cache key, it plays nicely with Rails' low level caching and adding something like the identity_cache gem can easily help with that.

I would like to see if other people have cleaner solutions to this though.

Problem

After studying DHH's and other blog articles about key-based cache expiration and Russian Doll Caching, I am still unsure how to handle one relation type. To be specific, a `has_many` relationship. I will share the results of my research on a sample app. It is a little bit of story telling, so hang on. Let's say we have the following ActiveRecord models. All we care about is a proper change of the model's `cache_key`, right? ``` class Article < ActiveRecord::Base attr_accessible :author_id, :body, :title has_many :comments belongs_to :author end class Comment < ActiveRecord::Base attr_accessible :article_id, :author_id, :body belongs_to :author belongs_to :article, touch: true end class Author < ActiveRecord::Base attr_accessible :name has_many :articles has_many :comments end ``` We already have one article, with one comment. Both by a different author. The goal is to have a change in the `cache_key` for the article in the following cases: - Article's body or title changes - Its comment's body changes - Article's author's name changes - Article's comment's author's name changes So by default, we are good for case 1 and 2. ``` 1.9.3-p194 :034 > article.cache_key => "articles/1-20130412185804" 1.9.3-p194 :035 > article.comments.first.update_attribute('body', 'First Post!') 1.9.3-p194 :038 > article.cache_key => "articles/1-20130412185913" ``` But not for case 3. ``` 1.9.3-p194 :040 > article.author.update_attribute('name', 'Adam A.') 1.9.3-p194 :041 > article.cache_key => "articles/1-20130412185913" ``` Let's define a composite `cache_key` method for `Article`. ``` class Article < ActiveRecord::Base attr_accessible :author_id, :body, :title has_many :comments belongs_to :author def cache_key [super, author.cache_key].join('/') end end 1.9.3-p194 :007 > article.cache_key => "articles/1-20130412185913/authors/1-20130412190438" 1.9.3-p194 :008 > article.author.update_attribute('name', 'Adam B.') 1.9.3-p194 :009 > article.cache_key => "articles/1-20130412185913/authors/1-20130412190849" ``` Win! But of course this does not work for case 4. ``` 1.9.3-p194 :012 > article.comments.first.author.update_attribute('name', 'Bernard A.') 1.9.3-p194 :013 > article.cache_key => "articles/1-20130412185913/authors/1-20130412190849" ``` So what options are left? We could do something with the `has_many` association on `Author`, but `has_many` does not take the `{touch: true}` option, and probably for a reason. I guess it could be implemented somewhat along the following lines. ``` class Author < ActiveRecord::Base attr_accessible :name has_many :articles has_many :comments before_save do articles.each { |record| record.touch } comments.each { |record| record.touch } end end article.comments.first.author.update_attribute('name', 'Bernard B.') article.cache_key => "articles/1-20130412192036" ``` While this does work. It has a huge performance impact, by loading, instantiating and updating every article and comment by that other, one by one. I don't believe it is a proper solution, but what is? Sure the 37signals use case / example might be different: `project -> todolist -> todo`. But I imagine a single todo item also belonging to a user. How would one solve this caching problem?

Original source