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?
articles.update_all(updated_at: Time.now)
, which would result in one op for articles (and one for comments). – Donnenfeldupdate_all
only executes SQL, no callbacks are performed so subsequenttouch
es will not happen, andcache_key
s on in memory objects are not regenerated. – Dessalinesreload
it. – Donnenfeldtouch
calls. If a user updates a his name and we callupdate_all
on his comments, thebelongs_to :article, touch: true
on the comment will not fire and the fragment cache for the article will not be expired. At least that's what I've seen, please correct me if I'm wrong. You could always expire all articles that a user has comments on by hand, but that would get hard to maintain as the tree gets larger. Unfortunately I don't see an alternative currently. – Dessalinesupdate_all
would not run the callbacks. I suppose you could add ahas_many :commented_articles through: :comments
type association to author, and touch those in the callback as well. But I see your point, russian doll does become messy very quickly when you're rendering content outside of a purely hierarchical structure (like the author's username). It is strange that you never see a mention of the solutions in russian doll caching writeups, like the linked 37Signals. – Donnenfeldcache_digest
is about managing template dependencies, tied to the template files themselves, which frees you from explicit versioning. I haven't used it, but I don't believe it has anything to do with this problem (non-hierarchical model data dependencies), and in fact, would suffer from it just the same. – Donnenfeld