Cobalt Edge

 
« Back to blog

Rails Action Caching With Query Parameters

First, if this is known to people, great, if not I hope it helps others, as I wasn't able to find this in all the Googling I did.  Furthermore, since this turned out to be such a simple solution, I'm curious if there are holes...

I wanted to setup more significant caching for some heavy use types of pages on DealBase.  Various things have made this challenging to date, but the last thing I ran into was dealing with the fact that query parameters change page results (duh), but that of course Rails' page and action caching ignore query parameters.  There isn't an easy (or?) way to get around the page caching part unless you start mucking with Nginx rules as well I think.  But, action caching has a solution.  I had done a ton of Googling on this, and I knew about adjusting the cache_path, as well as some other bits, but we have cases where there are a lot of parameters.  Plus, I didn't want to worry about what happens if I add a new search/filter type of parameter later on, and having to remember to add that to the list of things the cache_path differed on.  

As it turns out, you can simply do regular action caching, with full query parameter support, very easily with:

caches_action :my_action, :cache_path => Proc.new { |controller| controller.params }

That will do the regular style of action caching, but stick all your query parameters on (in alphabetical order so you don't have to worry about different query parameter order not retrieving the same cache results).  Now, it's quite likely that you'll want something a bit fancier.  For example, here's something closer to what I use in reality:

caches_action :action_one, :action_two,
  :cache_path => Proc.new { |c| c.params.delete_if { |k,v| k.starts_with?('utm_') } },
  :expires_in => 4.hours,
  :unless => Proc.new { |c| c.request.xml_http_request? || c.send(:current_user).try(:admin?) }

This just adds in some conditions, as well as removes some query parameters that I don't want to differentiate the cache on ("utm" keywords for Google Analytics, etc.).  In this particular case we're not caching the page at all if it's an AJAX request or for our admins.  

Finally, one comment is that you do need to be careful with things like pagination.  In many cases, page 1 is going to get viewed a ton, and maybe page 2, or page 17 for that matter are rarely viewed.  You could have a case where you make updates to the content that then makes a change such that the different pages are out of sync and thus you have duplicate items on pages or missing items.  You could skip using expires_in, and use cache sweepers if that works for you, but factor in how often updates are happening and whether that might nullify the advantage of caching (i.e. if you do frequent updates, it'd expire your cache a lot and maybe too often to benefit from it if you have high rate of updates).

What say you?  Anyone else doing this, any other issues?

Loading mentions Retweet

Comments (7)

Feb 24, 2010
Valentin said...
Thank you so much for sharing! I was searching everywhere for exactly that. Combining it with a short post http://seanbehan.com/ruby-on-rails/how-to-use-pretty-urls-with-rails-will_pag... I now have a nicely paginated and sortable list which is cached as good as possible.
Apr 29, 2010
mptre said...
Thanks! Just what I was looking for.
May 14, 2010
Charles Forcey said...
I am following Valentin's path here, but wanted to add my thanks too! Worked for me on Rails 3 Beta 3 with will_paginate 3.0 pre.
Jun 03, 2010
Simbul said...
That's been very helpful. However, how is one supposed to expire the cached actions? The fragment handlers look pretty different from the standard.
Jun 03, 2010
Simbul, the expiration depends on what cache store you're using. In the above example, you'll see this line/parameter to the caches_action:

:expires_in => 4.hours

That's one way to do it, but this only works with Memcache and maybe some others. We use Memcache, so that's how we do it.

If you aren't using Memcache, then you'll need to use either expire_action, or a "sweeper". expire_action lets you expire a particular page, and Sweepers are like observes and will run when objects change. See the Rails docs on these for the specifics to using them.

Jun 03, 2010
Simbul said...
Yes, I know that. Actually, I think my message was a little too dry to actually convey my question.

I'm using Sweepers, so I have to rely on expire_action() or expire_fragment(). The problem is, adding a :cache_path generates an unpredictable fragment key, something like:
views/host:port/things.xml?param1=value1¶m2=value2.xml

Since expiring each possible querystring explicitly would be a nightmare, I'm afraid the only possible solution is to pass a regexp to expire_fragment() - though I wanted to avoid that since it wouldn't work in some settings, e.g. with memcached.[1]

Btw, an additional difficulty is due to what I deem a Rails bug: the double ".xml" in the generated fragment key.

So, my real question would be: is there any way to implement this sweeper with an elegant solution?
Bonus points for: is the "xml" issue really a bug? How can it be solved?

[1] http://api.rubyonrails.org/classes/ActionController/Caching/Fragments.html#M0...

Jun 03, 2010
Right, that is harder, given regex vs. Memcache, etc. For us, our data and system is such that we can really take advantage of the purely time based expiration. We use time based expiration for almost everything, with a few exceptions, none of which use this particular type of caching (and thus are trivial to expire just using the regular expire_ methods).

I think the regex is likely quite solvable (you can at least leverage that one will be before the query/question mark, one after).

If you find a good solution, please do add another comment.

Leave a comment...

 
Got an account with one of these? Login here, or just enter your comment below.
Posterous-login    twitter