Rails Caching
November 16th, 2006 -
I’ve been working with Ryan over at Moral Metric for about 2 months now. It is a great project and one that I am becoming more and more passionate about. Not only is it a great idea, but we get to code it in Ruby on Rails. And let’s not forgot that I get to work with my friend Ryan to boot!
We have a fairly sophisticated rights and roles management for our users on Moral Metric that keeps a tight control on who can do what. It is heavily based on the CRUD principle. We needed this because we have 6 levels of access that all vary in what the user can or cannot do. The problem that we found with this is that we are hitting the database pretty hard to check if they user has permission to do this or that. So, the obvious choice was to implement caching which became my job to implement, something I have not done before in Rails – till now.
My first thought was to look into Page, Action and Fragment Caching that are bundled with Rails. I spent a good amount of time reading the API and also going over various blogs that talked about. More I read more I felt that it wasn’t the solution we needed, but to get a good grasp for it, for when we need more caching later, I spent some time working with it and testing it out.
Page Caching is great is you need to cache an entire HTML page that will remain stateless. You would use page caching like so:
class AboutController < ApplicationController
caches_page :index, :show
end
Simple, huh? Now, if you want to expire the cache you would do the following:
expire_page :action => "show", :id => params[:about][:id]
Nothing much to it and it works really well….for stateless data where all users get treated the same. Obviously that wasn’t going to work for what we wanted to do.
Next, I worked with Action Caching, which is really like Page Caching except with Page Caching every request does not go through Action Pack. What this means is that Page Caching bypasses any filters you might have running on your data. Action Caching will honor your filters and run through them. With action caching you can do user authentication and user restrictions. Hm, sounds a bit more like what we were looking for, but not quite.
Here is how you would do Action Caching:
class AboutController < ApplicationController
caches_action :index, :show
end
We’ll talk in just a minute about expiring that cache, but first let’s talk about Fragment Caching which is pretty cool.
Fragment Caching is to be used within your template or view files. What you can do is cache “fragments” of your view code. Here is what it looks like:
<% cache do %>
<%= render :partial => "page", :collection => @abouts %>
<% end %>
That partial will now be cached, but where does all this stuff go, when it is cached? Page/Action Caching is typically stored within your public directory automatically. For Fragment Caching, you will need to set this yourself by adding the following to your environment:
ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
Alright, let’s talk about expiring our cache. I’ve showed you a very simple method when you are using page caching, here I’d like to show you how to write a Cache Sweeper. Basically, when you cache something, your application will do one call to get the data and then after it is cached, it will pull up the cached data, or the file that has been created on all of the next requests. We need a way to expire our cache so that when we make updates or changes to the data, they will be reflected in our views. A good way to do this is to create a sweeper that will do just that, expire our cache if our data is updated. Here is what an example sweeper would look like:
class AboutSweeper < ActionController::Caching::Sweeper
observe About
def after_create(data)
expire_about(data)
end
def after_save(data)
expire_about(data)
end
def after_destroy(data)
expire_about(data)
end
def expire_about(data)
FileUtils.rm_rf File.expand_path("public/about/#{data.id}", RAILS_ROOT)
FileUtils.rm_rf File.expand_path("public/index.html", RAILS_ROOT)
end
end
Thanks Geoff for making the code for the Rails Podcast publicly available. I learned a bit on caching from looking at his source.
You want to keep this file in your apps model direction: RAILS_ROOT/app/models and name it about_sweeper.rb or whatever name you name your class.
The above code is pretty straight forward. After an object has been saved or deleted it calls the expire_about method to remove the cached files from the public directory.
Well, those are the three caching systems included within Rails, but none of them did exactly what I needed. What we needed to do was cache 2 methods within a model that aren’t associated with a page per say, but with many pages and actions within those pages. We had thought about memcaching, but that would be way too premature at this stage. So, I had began thinking about writing my own when I stumbled upon Yurii Rashkovski’s website and saw that he created a simple caching rb file. He has a great explanation to get it working that is very simple.
Since implementing this caching technique, Moral Metric has really increased its speed. We’ve cut down on the calls to the database to check user permissions significantly and when we make an update to the permissions, changes take effect seemlessly. This definately made my day to see the speed be increased quite drastically.
Update: Check out Yurii’s wiki page on the Caches.rb class. Thanks for the heads up Ryan.
Tags: development, rails, tutorials
Comments
Robert
November 18th, 2006
Thanks Ryan. I’ve added the link to the Caches.rb wiki link to the end of the article.
Yannick
November 20th, 2006
Nice article Robert. I’ve heard about caching but never really had the need to use it, however, the article was very informative and it’s interesting to see just how easy it is to do caching in Rails.
Keep up the good work.
Robert
November 20th, 2006
Thanks Yannick. Rails really does make it much easier and the community is just awesome!
Yurii Rashkovskii
December 18th, 2006
Just to note, Caches.rb is gem/plugin installable already (http://pad.verbdev.com/cachesrb/show/HowToGet)
Robert
December 18th, 2006
Thanks for the note, Yurii and thanks for the cool gem!
David
January 16th, 2007
Shouldn’t those calls to “expire_episode(data)” in your example within the after_* definitions actually be to “expire_about(data)”? or alternatively the expire_about definition should actually be named expire_episode?
or am I being daft? { which is possible :) }
Robert
January 16th, 2007
Nope, you aren’t being daft. Thanks for pointing out my typing mistake. It has been fixed.
Commenting has been turned off.
Ryan Heneise
November 18th, 2006
Great article Robert! I’ve never spent much time looking into caching, so your concise explanation was very helpful. Yurii also has a little wiki on caching.rb here: http://pad.verbdev.com/cachesrb/show/HomePage