1

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.

Rajesh Kolappakam
  • 2,095
  • 14
  • 12
  • 1
    see if this gem helps https://github.com/Shopify/identity_cache – AJcodez Sep 23 '13 at 08:16
  • @AJcodez, thanks for the gem, its a nice gem. Its basically a generic implementation of the `find_in_cache` and `cached` methods that I added, so it saves me from repeating those in each model. However, it is as intrusive as my solution. I have to use the `cache` methods instead of default active record methods. – Rajesh Kolappakam Sep 23 '13 at 08:47
  • I mean you have to use different methods, but you don't want the default active record functionality so you probably can't be using the default active record moethds – AJcodez Sep 23 '13 at 16:57
  • Makes sense, would you please submit as an answer so I can give you credit – Rajesh Kolappakam Sep 23 '13 at 17:31

1 Answers1

1

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
AJcodez
  • 31,780
  • 20
  • 84
  • 118