使用has_many关系为俄罗斯玩偶缓存构建Rails应用程序

时间:2013-04-12 20:13:03

标签: ruby-on-rails ruby caching fragment-caching russian-doll-caching

在研究了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. 文章评论的作者姓名更改
  5. 因此,默认情况下,我们对案例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"
    

    让我们为cache_key定义一个复合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"
    

    赢!但当然这对案例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"
    

    那么剩下什么选择呢?我们可以对has_many上的Author关联执行某些操作,但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。但我想象一个属于用户的todo项目。

    如何解决这个缓存问题?

2 个答案:

答案 0 :(得分:8)

我偶然发现的一种方法是通过缓存键来处理这个问题。为文章添加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'low level caching一起使用,并且添加identity_cache gem之类的内容可以轻松地帮助解决这个问题。

我想看看其他人是否有更清洁的解决方案。

答案 1 :(得分:0)

正如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并触摸类似https://gist.github.com/tispratik/9276110的方法,以便将更新的时间戳存储在memcached而不是数据库的updated_at列中。

另请注意,我无法使用Rails中的默认cache_timestamp_format,因为它提供的时间戳只有几秒钟。我觉得需要更精细的时间戳,所以我选择:nsec(纳秒)。

cache_timestamp_format的时间戳:20140227181414
带nsec的时间戳:20140227181414671756000