Rails Cache Key generated as ActiveRecord::Relation

妖精的绣舞 提交于 2019-12-23 13:08:48

问题


I am attempting to generate a fragment cache (using a Dalli/Memcached store) however the key is being generated with "#" as part of the key, so Rails doesn't seem to be recognizing that there is a cache value and is hitting the database.

My cache key in the view looks like this:

cache([@jobs, "index"]) do

The controller has:

@jobs = @current_tenant.active_jobs

With the actual Active Record query like this:

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

Looking at the rails server, I see the cache read, but the SQL Query still runs:

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

Any ideas as to what I might be doing wrong? I'm sure it has to do w/ the key generation and ActiveRecord::Relation, but i'm not sure how.


回答1:


I have had similar problems, I have not been able to successfully pass relations to the cache function and your @jobs variable is a relation.

I coded up a solution for cache keys that deals with this issue along with some others that I was having. It basically involves generating a cache key by iterating through the relation.

A full write up is on my site here.

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

In summary I added a get_cache_keys function to ActiveRecord::Base

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)

I can now create cache fragments by doing

cache(@model.get_cache_key(['textlabel'])) do



回答2:


Background:

The problem is that the string representation of the relation is different each time your code is run:

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

So you get a different cache key each time.

Besides that it is not possible to get rid of database queries completely. (Your own answer is the best one can do)

Solution:

To generate a valid key, instead of this

cache([@jobs, "index"])

do this:

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

This queries the database and builds an array of the models, from which the cache_key is retrieved.

PS: I could swear using relations worked in previous versions of Rails...




回答3:


We've been doing exactly what you're mentioning in production for about a year. I extracted it into a gem a few months ago:

https://github.com/cmer/scope_cache_key

Basically, it allows you to use a scope as part of your cache key. There are significant performance benefits to doing so since you can now cache a page containing multiple records in a single cache element rather than looping each element in the scope and retrieving caches individually. I feel that combining this with with the standard "Russian Doll Caching" principles is optimal.




回答4:


I've done something like Hopsoft, but it uses the method in the Rails Guide as a template. I've used the MD5 digest to distinguish between relations (so User.active.cache_key can be differentiated from User.deactivated.cache_key), and used the count and max updated_at to auto-expire the cache on updates to the relation.

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



回答5:


While I marked @mark-stratmann 's response as correct I actually resolved this by simplifying the implementation. I added touch: true to my model relationship declaration:

belongs_to :tenant, touch: true

and then set the cache key based on the tenant (with a required query param as well):

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

That way if a new Job is added, it touches the Tenant cache as well. Not sure if this is the best route, but it works and seems pretty simple.




回答6:


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



回答7:


maybe this can help you out https://github.com/casiodk/class_cacher , it generates a cache_key from the Model itself, but maybe you can use some of the principles in the codebase




回答8:


As a starting point you could try something like this:

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

I'm having normalized database where multiple models relate to the same other model, think of clients, locations, etc. all having addresses by means of a street_id.

With this solution you can generate cache_keys based on scope, e.g.

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

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

and I could simply modify self.updated from above to also include associated objects (because has_many does not support "touch", so if I updated the street, it won't be seen by the cache otherwise):

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

As long as you don't "undelete" records and use touch in belongs_to, you should be alright with the assumption that a cache key made of count and max updated_at is sufficient.




回答9:


I'm using a simple patch on ActiveRecord::Relation to generate cache keys for relations.

require "digest/md5"

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

ActiveRecord::Relation.send :include, RelationCacheKey


来源:https://stackoverflow.com/questions/15194270/rails-cache-key-generated-as-activerecordrelation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!