How do I cache a method with Ruby/Rails?

Viewed 13783

I have an expensive (time-consuming) external request to another web service I need to make, and I'd like to cache it. So I attempted to use this idiom, by putting the following in the application controller:

def get_listings
  cache(:get_listings!)
end

def get_listings!
  return Hpricot.XML(open(xml_feed))
end

When I call get_listings! in my controller everything is cool, but when I call get_listings Rails complains that no block was given. And when I look up that method I see that it does indeed expect a block, and additionally it looks like that method is only for use in views? So I'm guessing that although it wasn't stated, that the example is just pseudocode.

So my question is, how do I cache something like this? I tried various other ways but couldn't figure it out. Thanks!

8 Answers

Late to the party, but in case someone arrives here searching.

I use to carry this little module around from project to project, I find it convenient and extensible enough, without adding an extra gem. It uses the Rails.cache backend, so please use it only if you have one.

# lib/active_record/cache_method.rb
module ActiveRecord
  module CacheMethod
    extend ActiveSupport::Concern

    module ClassMethods
      # To be used with a block
      def cache_method(args = {})
        @caller = caller
        caller_method_name = args.fetch(:method_name)     { @caller[0][/`.*'/][1..-2] }
        expires_in         = args.fetch(:expires_in)      { 24.hours }
        cache_key          = args.fetch(:cache_key)       { "#{self.name.underscore}/methods/#{caller_method_name}" }

        Rails.cache.fetch(cache_key, expires_in: expires_in) do
          yield
        end
      end
    end

    # To be used with a block
    def cache_method(args = {})
      @caller = caller
      caller_method_name = args.fetch(:method_name) { @caller[0][/`.*'/][1..-2] }
      expires_in         = args.fetch(:expires_in)  { 24.hours }
      cache_key          = args.fetch(:cache_key)   { "#{self.class.name.underscore}-#{id}-#{updated_at.to_i}/methods/#{caller_method_name}" }

      Rails.cache.fetch(cache_key, expires_in: expires_in) do
        yield
      end
    end
  end
end

Then in an initializer:

# config/initializers/active_record.rb
require 'active_record/cache_method'
ActiveRecord::Base.send :include, ActiveRecord::CacheMethod

And then in a model:

# app/models/user.rb
class User < AR 
  def self.my_slow_class_method
    cache_method do 
      # some slow things here
    end
  end

  def this_is_also_slow(var)
    custom_key_depending_on_var = ...
    cache_method(key_name: custom_key_depending_on_var, expires_in: 10.seconds) do 
      # other slow things depending on var
    end
  end
end

At this point it only works with models, but can be easily generalized.

Related