【问题标题】:Rails Cache Key generated as ActiveRecord::RelationRails 缓存键生成为 ActiveRecord::Relation
【发布时间】:2013-02-18 02:39:45
【问题描述】:

我正在尝试生成一个片段缓存(使用 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 Query 仍在运行:

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,但我不确定如何。

【问题讨论】:

    标签: ruby-on-rails caching memcached dalli


    【解决方案1】:

    我也遇到过类似的问题,我无法成功地将关系传递给缓存函数,而您的 @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
    

    【讨论】:

      【解决方案2】:

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

      belongs_to :tenant, touch: true
      

      然后根据租户设置缓存键(也需要查询参数):

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

      这样,如果添加了新作业,它也会触及租户缓存。不确定这是否是最好的路线,但它很有效,而且看起来很简单。

      【讨论】:

        【解决方案3】:

        背景:

        问题是每次运行代码时关系的字符串表示形式都不同:

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

        所以你每次都会得到一个不同的缓存键。

        此外,不可能完全摆脱数据库查询。 (你的own answer 是最好的)

        解决方案:

        生成一个有效的密钥,而不是这个

        cache([@jobs, "index"])
        

        这样做:

        cache([@jobs.to_a, "index"])
        

        这会查询数据库并构建模型数组,从中检索cache_key

        PS:我可以发誓使用以前版本的 Rails 中的关系...

        【讨论】:

        • 如果模型上的字段之一是小数,我不太确定这是否可行
        • @bcackerman:那没什么区别。默认情况下,Rails 使用updated_atid only 来生成缓存键。
        【解决方案4】:

        大约一年来,我们一直在按照您在生产中提到的那样做。几个月前我把它提取到了一个宝石中:

        https://github.com/cmer/scope_cache_key

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

        【讨论】:

          【解决方案5】:

          也许这可以帮助你 https://github.com/casiodk/class_cacher ,它从模型本身生成一个cache_key,但也许你可以使用代码库中的一些原理

          【讨论】:

            【解决方案6】:

            我正在使用此代码:

            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
            

            【讨论】:

              【解决方案7】:

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

              require "digest/md5"
              
              module RelationCacheKey
                def cache_key
                  Digest::MD5.hexdigest to_sql.downcase
                end
              end
              
              ActiveRecord::Relation.send :include, RelationCacheKey
              

              【讨论】:

              • 如果任何模型被更新,这将永远不会使片段过期,因为 SQL 本身被用作缓存键。
              【解决方案8】:

              我做过类似 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
              

              【讨论】:

                【解决方案9】:

                作为起点,您可以尝试以下方法:

                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
                

                我有一个规范化的数据库,其中多个模型与同一个其他模型相关,考虑客户、位置等,所有这些都通过 street_id 获得地址。

                使用此解决方案,您可以根据范围生成 cache_keys,例如

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

                我可以简单地从上面修改self.updated 以包含关联对象(因为has_many 不支持“触摸”,所以如果我更新了街道,否则缓存不会看到它):

                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
                

                只要您不“取消删除”记录并在 belongs_to 中使用 touch,您应该可以假设由 count 和 max updated_at 组成的缓存键就足够了。

                【讨论】:

                  猜你喜欢
                  • 2013-02-14
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-03-08
                  • 2014-05-23
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多