Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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:

  1. Article's body or title changes
  2. Its comment's body changes
  3. Article's author's name changes
  4. 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?

like image 781
mlangenberg Avatar asked Apr 12 '13 20:04

mlangenberg


1 Answers

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.

like image 116
Graham Conzett Avatar answered Oct 21 '22 06:10

Graham Conzett