Cache key for collections in ActiveRecord

I've been working on the performance of a Rails application and much of my recent work involves simply caching. It was an application that was an excellent proof-of-concept which quickly turned into a production application.

We've got memcached setup and running and now it's a speedy little interface.

But one thing I knew I needed but wasn't sure how to do was cache a group of records. Rails fragment caching is super simple for one record:

<% cache [object] do %>
  ... this will be cached based upon your object.cache_key ...
<% end >

That's fine and dandy until you want to cache the view for not just one object in your loop, but the entire rendered view of @objects. As much as I looked around, I found that nobody has written about this problem. I still wonder if I'm being naive and there is some obvious way to do this that has simply slipped by me.

When I first tackled this, I had just a handful of objects in one particular view and would never have more, so I just concatenated the cache_keys of all of them and I was done. In another instance (with a larger number of objects) I tried something different by prepending another object which could be changing and appending a string: cache [user_role, @project_application,'task_lists'] do. This was painful because it required that I ensure that all the objects down the relationship tree call :touch and update their belongs_to parent until finally my @project_application was updated.

The task lists in this application don't just have tasks, they have attachments and notes too and this just felt like the invalidation of the cache was far too intertwined among the related objects for my liking.

Finally I realized that it was much simpler than that. What I need here is pretty simple: a cache_key for a collection.

A group_key method on a collection would be so much simpler than any convoluted process I attempted. Here's what I did:

def self.group_key
  count = ActiveRecord::Base.connection.select_value("select count(*) from widgets").to_s
  timestamp = ActiveRecord::Base.connection.select_value("select max(updated_at) from widgets").to_s.parameterize.wrapped_string
  count+'-'timestamp
end
That makes 2 simple calls to the database to get a parameterized representation of the most recent count and `updated_at` value for my Widget. If any of my objects change for any reason or if I add a new record, my group_key will be updated. I took this and put together a simple gem called `group_cache_key` which you may find on [gemcutter.org](http://gemcutter.org/gems/group_cache_key). The source is a bit different in that it includes a max `updated_at` and max `created_at` and it doesn't go back to the database for the information:
def cache_key
  if self.empty?
    'empty'
  else
    update_timestamp = max {|a,b| a.updated_at <=> b.updated_at }.updated_at.to_i.to_s
    create_timestamp = max {|a,b| a.created_at <=> b.created_at }.created_at.to_i.to_s
    self.first.class.to_s.underscore+'/'+length.to_s+'-'+create_timestamp+'-'+update_timestamp
  end
end

I hope you find it useful. It'll give you a cache key value of something like widget/2-1253224342-1253311589.

To use it, grab the gem (gem install group_cache_key) and add this to your config/environment.rb:

config.gem `group_cache_key`

Let me know what you think.

Update

I've pushed a newer version of the gem out there which creates a hash of the given ids. The hash is created in the order given, so if you're doing a sort in your interface, you'll get an entirely different cache_key.

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.