Rails缓存键生成为ActiveRecord::Relation

4
我正在尝试生成一个片段缓存(使用Dalli/Memcached存储),但是键被生成为包含“#”的一部分,因此Rails似乎无法识别缓存值并且正在访问数据库。
我的视图中缓存键看起来像这样:
cache([@jobs, "index"]) do

控制器拥有:

@jobs = @current_tenant.active_jobs

使用下面这个实际的Active Record查询:

def active_jobs
   self.jobs.where("published = ? and expiration_date >= ?", true, Date.today).order("(featured and created_at > now() - interval '" + self.pinned_time_limit.to_s + " days') desc nulls last, created_at desc")
end

查看Rails服务器,我发现缓存已经被读取,但是SQL查询仍在运行:

Cache read: views/#<ActiveRecord::Relation:0x007fbabef9cd58>/1-index 
Read fragment views/#<ActiveRecord::Relation:0x007fbabef9cd58>/1-index (1.0ms)
(0.6ms) SELECT COUNT(*) FROM "jobs" WHERE "jobs"."tenant_id" = 1 AND (published = 't' and expiration_date >= '2013-03-03')
  Job Load (1.2ms)  SELECT "jobs".* FROM "jobs" WHERE "jobs"."tenant_id" = 1 AND (published = 't' and expiration_date >= '2013-03-03') ORDER BY (featured and created_at > now() - interval '7 days') desc nulls last, created_at desc

有什么想法是我可能做错了吗?我确定这与密钥生成和ActiveRecord::Relation有关,但我不确定如何解决。

9个回答

8

背景:

问题在于每次运行代码时,关系的字符串表示都不同:

                                 |This changes| 
views/#<ActiveRecord::Relation:0x007fbabef9cd58>/...

所以每次都得到不同的缓存键。

除此以外,无法完全摆脱数据库查询。(您的自己的答案是最好的解决方案)

解决方案:

为了生成有效的键,而不是使用这个

cache([@jobs, "index"])

做这个:
cache([@jobs.to_a, "index"])

这会查询数据库并构建模型数组,从中检索cache_key
PS:我敢肯定在之前的Rails版本中使用关系是可行的...

我不太确定如果模型中的一个字段是十进制数,这个方法是否有效。 - bcackerman
@bcackerman:这不会有任何区别。默认情况下,Rails 仅使用“updated_at”和“id”来生成缓存键。 - Daniel Rikowski

3

我们已经在生产环境中做了你提到的事情大约一年了。我几个月前将其提取为一个宝石:

https://github.com/cmer/scope_cache_key

基本上,它允许您将作用域作为缓存键的一部分使用。这样做有显著的性能优势,因为现在您可以在单个缓存元素中缓存包含多个记录的页面,而不是循环作用域中的每个元素并逐个检索缓存。我认为结合标准的“俄罗斯套娃缓存”原则是最佳选择。

2
我做了类似Hopsoft的东西,但是使用Rails Guide中的方法作为模板。我使用了MD5摘要来区分关系(这样User.active.cache_key就可以与User.deactivated.cache_key区分开),并使用计数和最大的updated_at来在关系更新时自动过期缓存。
require "digest/md5"

module RelationCacheKey
  def cache_key
    model_identifier = name.underscore.pluralize
    relation_identifier = Digest::MD5.hexdigest(to_sql.downcase)
    max_updated_at = maximum(:updated_at).try(:utc).try(:to_s, :number)

    "#{model_identifier}/#{relation_identifier}-#{count}-#{max_updated_at}"
  end
end

ActiveRecord::Relation.send :include, RelationCacheKey

2
我遇到了类似的问题,无法成功将关系传递给缓存函数,而您的@jobs变量是一个关系。
我编写了一个解决方案来处理这个问题以及我遇到的其他一些问题。它基本上涉及通过迭代关系生成缓存键。
我的网站上有完整的说明。

http://mark.stratmann.me/content_items/rails-caching-strategy-using-key-based-approach

总的来说,我向ActiveRecord::Base添加了一个get_cache_keys函数。
module CacheKeys
  extend ActiveSupport::Concern
  # Instance Methods
    def get_cache_key(prefix=nil)
      cache_key = []
      cache_key << prefix if prefix
      cache_key << self
      self.class.get_cache_key_children.each do |child|
        if child.macro == :has_many
          self.send(child.name).all.each do |child_record|
            cache_key << child_record.get_cache_key
          end
        end
        if child.macro == :belongs_to
          cache_key << self.send(child.name).get_cache_key
        end
      end
      return cache_key.flatten
    end

  # Class Methods
  module ClassMethods
    def cache_key_children(*args)
      @v_cache_key_children = []
      # validate the children
      args.each do |child|
        #is it an association
        association = reflect_on_association(child)
        if association == nil
          raise "#{child} is not an association!"
        end
        @v_cache_key_children << association
      end
    end

    def get_cache_key_children
      return @v_cache_key_children ||= []
    end

  end
end

# include the extension
ActiveRecord::Base.send(:include, CacheKeys)

我现在可以通过执行以下操作创建缓存片段:
cache(@model.get_cache_key(['textlabel'])) do

1

Im using this code:

class ActiveRecord::Base
  def self.cache_key
    pluck("concat_ws('/', '#{table_name}', group_concat(#{table_name}.id), date_format(max(#{table_name}.updated_at), '%Y%m%d%H%i%s'))").first
  end

  def self.updated_at
    maximum(:updated_at)
  end
end

1

尽管我将 @mark-stratmann 的回复标记为正确,但我实际上通过简化实现来解决了这个问题。我在模型关系声明中添加了 touch: true:

belongs_to :tenant, touch: true

然后根据租户设置缓存键(还需要一个必需的查询参数):

<% cache([@current_tenant, params[:query], "#{@current_tenant.id}-index"]) do %>

那样,如果添加了新工作,它也会触及租户缓存。不确定这是否是最佳路线,但它有效且似乎相当简单。

0

作为起点,您可以尝试类似于以下的代码:

def self.cache_key
  ["#{model_name.cache_key}-all",
   "#{count}-#{updated_at.utc.to_s(cache_timestamp_format) rescue 'empty'}"
  ] * '/'
end

def self.updated_at
  maximum :updated_at
end

我有一个规范化的数据库,其中多个模型与同一其他模型相关联,例如客户、位置等都通过街道ID拥有地址。

使用此解决方案,您可以根据作用域生成缓存键,例如:

cache [@client, @client.locations] do
  # ...
end

cache [@client, @client.locations.active, 'active'] do
  # ...
end

我可以简单地修改上面的 self.updated,以便还包括相关对象(因为 has_many 不支持 "touch",所以如果我更新了街道,否则缓存就看不到它):

belongs_to :street

def cache_key
  [street.cache_key, super] * '/'
end

# ...

def self.updated_at
  [maximum(:updated_at),
   joins(:street).maximum('streets.updated_at')
  ].max
end

只要不“undelete”记录并在belongs_to中使用touch,您就可以假设由计数和最大updated_at组成的缓存键是足够的。

0

-1

我正在使用一个简单的补丁来为关系生成缓存键。

require "digest/md5"

module RelationCacheKey
  def cache_key
    Digest::MD5.hexdigest to_sql.downcase
  end
end

ActiveRecord::Relation.send :include, RelationCacheKey

如果任何一个模型被更新,由于SQL本身被用作缓存键,因此这将永远不会使片段过期。 - fivedigit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接