用has_many关系构建一个俄罗斯娃娃缓存的Rails应用



在研究了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. 它的注释主体变化
  3. 文章作者姓名变更
  4. 文章评论的作者姓名变更

所以默认情况下,我们适用于情况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

最新更新