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:
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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With