在研究了DHH和其他关于基于键的缓存过期和俄罗斯娃娃缓存的博客文章之后,我仍然不确定如何处理一种关系类型。具体来说,是一个has_many
关系。
我将分享我对一个示例应用程序的研究结果。这是一个小故事,所以请稍等。假设我们有以下ActiveRecord模型。我们只关心模型cache_key
的适当变化,对吧?
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
我们已经有一篇文章,一条评论。都是不同的作者写的。目标是在以下情况下对文章的cache_key
进行更改:
- 文章正文或标题更改
- 它的注释主体变化
- 文章作者姓名变更
- 文章评论的作者姓名变更
所以默认情况下,我们适用于情况1和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"
但不适用情况3。
1.9.3-p194 :040 > article.author.update_attribute('name', 'Adam A.')
1.9.3-p194 :041 > article.cache_key
=> "articles/1-20130412185913"
我们为Article
定义一个复合cache_key
方法。
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"
赢!但是这当然不适用于情形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"
那么还剩下什么选择呢?我们可以在Author
上做一些has_many
关联的事情,但是has_many
没有采取{touch: true}
选项,可能是有原因的。我想它可以按照以下方式实现。
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"
虽然这确实有效。它有巨大的性能影响,通过加载,实例化和更新每一篇文章和评论,一个接一个。我不认为这是一个合适的解决方案,但什么才是呢?
当然37signals用例/示例可能会有所不同:project -> todolist -> todo
。但是我想象一个单独的待办事项也属于一个用户。
如何解决这个缓存问题?
我偶然发现的一种方法是通过缓存键来处理这个问题。为文章的评论者添加has_many_through
关系:
class Article < ActiveRecord::Base
attr_accessible :author_id, :body, :title
has_many :comments
has_many :commenters, through: :comments, source: :author
belongs_to :author
end
然后在article/show中,我们将像这样构造缓存键:
<% 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 %>
诀窍在于,无论何时添加、删除或更新注释,从commenters
关联生成的缓存键都会更改。虽然这确实需要额外的SQL查询来生成缓存键,但它与Rails的低级缓存配合得很好,添加identity_cache之类的gem可以很容易地帮助实现这一点。
我想看看其他人是否有更清晰的解决方案。
此处建议https://rails.lighthouseapp.com/projects/8994/tickets/4392-add-touch-option-to-has_many-associations,在我的情况下,我只是创建了一个after_save回调来更新相关对象的时间戳。
def touch_line_items_and_tactics
self.line_item_advertisements.all.map(&:touch)
end
顺便说一句,我们在一个遗留数据库上构建了rails应用程序,该数据库的列名为last_modified_time
,它的语义是"当用户最后修改它时"。因此,由于不同的语义,我们不能使用开箱即用的:touch
选项。我必须monkeypatch cache_key和touch方法,如https://gist.github.com/tispratik/9276110,以便将更新的时间戳存储在memcached中,而不是数据库的updated_at列中。还请注意,我不能使用默认的cache_timestamp_format
从Rails,因为它提供的时间戳只有秒。我觉得需要一个更细粒度的时间戳,所以我选择了:nsec(纳秒)。
Timestamp with cache_timestamp_format: 20140227181414
Timestamp with nsec: 20140227181414671756000