Question

I have a languages table that hardly every changes. I am trying to avoid database queries on this table other the initial caching.

class Language < ActiveRecord::Base
  attr_accessible :code, :name, :native_name

  def self.find_by_name_in_cache(name)
    get_all_cached.find {|l| l.name == name}
  end

  def self.find_by_code_in_cache(code)
    get_all_cached.find {|l| l.code == code}
  end

  def self.find_by_id_in_cache(id)
    get_all_cached.find {|l| l.id == id}
  end

  def self.get_all_cached
    Rails.cache.fetch('all_languages') {Language.all}
  end
end

All goes fine as long as I am using one of the find_in_cache methods that I have defined.

My question is, how can I force ActiveRelation to use the caching as well.

For example, consider the following user model:

class User < ActiveRecord::Base
  belongs_to :native_language, :class_name => :Language, :foreign_key => :native_language_id
end

When I access @user.native_language, it queries language from the database. I would highly appreciate any ideas to prevent this.

I know I can do the following:

class User < ActiveRecord::Base
  belongs_to :native_language, :class_name => :Language, :foreign_key => :native_language_id

  def native_language_cached
    Language.find_by_id_in_cache(self.native_language_id)
  end
end

However, I was hoping for a more transparent solution as a lot of my tables reference languages table and it would be so messy to add the cached methods to all these models.

Was it helpful?

Solution

To cache the query during a single request, you don't have to do anything besides make sure you're using the ActiveRecord::QueryCache middleware. If you call it twice:

2.times{ user.native_language.to_s }

You'll see something like this in your log:

Language Load (0.2ms)   SELECT `languages`.* FROM `languages` WHERE ...
CACHE (0.0ms)  SELECT `languages`.* FROM `languages` WHERE ...

Caching across requests requires manual caching. The identity_cache gem might be useful for what you're trying to do (cache associations).

The quickest way would probably just to add the expires_in option to the code you have. You can write a generic cached_find method like the following:

def self.cached_find(id)
  key = model_name.cache_key + '/' + id
  Rails.cache.fetch(key) do
    find(id).tap do |model|
      Rails.cache.write(key, model, expires_in: cache_period)
    end
  end
end

def self.cache_period
  3.days
end

You can make it a module mixin to use with as many models as necessary. It does mean you have to write your own association finds. You can also do it with callbacks:

after_commit do
  Rails.cache.write(cache_key, self, expires_in: self.class.cache_period)
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top