The Basics of Caching and Cache Digests

This is an introduction to caching in Rails.  You may know that Rails performs some caching “automagically”, but you aren’t quite sure what that means or what that is. Read on to find out.

In a nutshell, caching gives us quick access to values without hitting a more expensive store, such as a database. In this manner, we can improve the performance of our Rails app. Rails will cache almost anything, from an object to a page to a partial.

Rails mainly provide three techniques of caching.

  • Page caching
  • Action caching
  • Fragment caching

Before we get started, be sure you have enabled caching in your environment file.

config.action_controller.perform_caching = true

It’s true by default for production.rb but false for development.rb and test.rb.

Page Caching

Page Caching is a mechanism which stores the  output of an action in a file. so when a request comes in the web server can respond without going through Actionpack . To try this out, I have created a sample application . I generated a scaffold blog with title and  description attributes. In our blogs controller, we have an index action and we want to cache that.  To accomplish this, call caches_page method for the action we wish to cache.

class BlogsController < ApplicationController
  caches_page :index
  def index
    @blogs = Blog.all
  end
end

Rails create a file in the /public directory called blogs.html  When we start our server and make a request for ‘/blogs’, Rails caches the first vist to the page in the aforementioned blogs.html file.  Any time after that, you will see that cached file, as shown in the following log entry:

Write page /Users/rashmiyadav/crap/cache_digest_test/public/blogs.html (0.7ms)

If you want to cache another action, just add it to method caches_page

caches_page :index, :show

In order to expire the page so we can cache a new version, issue a call to  expire_page:

class BlogsController < ApplicationController
  caches_page :index
  def  index
    @blogs = Blog.all
  end
  def create
    expire_page :action => :index
    @blog = Blog.new(params[:blog])
    @blog.save
  end
  def update
    expire_page :action => :index
    @blog = Blog.find(params[:id])
    @blog.update_attributes(params[:blog])
  end
end

Anytime we update or create a record it will expire the index cache. You can validate this, again, using the logs :

Expire page /Users/rashmiyadav/crap/cache_digest_test/public/cache/blogs.html (0.2ms)  

The expire_page method takes a number of arguments, such as  the path of cached page like ‘/blogs’, or a hash with action, controller name, and format to expire.

 expire_page :controller => 'blogs', :action  => 'show', :format => 'json'

Configuration

By default, Rails stores cache files in the /public directory, but we can change it. Simple change the page_cache_directory value in config/application.rb file:

config.action_controller.page_cache_directory = Rails.public_path + "/cache" 

Now all cache files will be in the /public/cache directory. One thing to remember, if you are running the server locally in development  and you have old cache files in you public directory, the server will continue to pick up the old cache files in the public directory. In other words,  don’t forget to remove the old cache files from the public directory.

Another configuration item for page caching is the extension of the cache file. The extension of a cache file depends on the incoming request. If it’s json, then cache file will be ‘.json’ and if the request is xml  then the cache file will be ‘.xml’ and so on. If a request does not have any specific extension then, by default , it will get a ‘.html’ extension. We can configure this using config.action_controller.page_cache_extension.

Page Caching is one way to speed up your application, but it is not an option if you have content that sits behind authentication.  Let’s look at the other methods of caching in Rails.

Action Caching

Action Caching is similar to Page Caching, except that filters are executed before the caching occurs. Let’s take our blog application example:

class BlogsController < ApplicationController
  before_filter :authenticate
  caches_action :index
  def index
    @blogs = Blog.all
  end
end

we call caches_action in order to cache the  index action. The first time we hit the link http://localhost:3000/blogs results in the following blog entry :

Write fragment views/localhost:3000/blogs (1.8ms)

Now, every time we call the index action,  it  will run the authenticate method before giving the cached response.  This is the main difference between action and page caching . Action caching runs all the filters before serving cache.

Expire the Cache

To expire the cache, we just need to call expire_action:

class BlogsController < ApplicationController
  caches_action :index
  def index
    @blogs = Blog.all
  end
  def create
    expire_action :action => :index
    @blog = Blog.new(params[:blog])
    @blog.save
  end
  def update
    expire_action :action => :index
    @blog = Blog.find(params[:id])
    @blog.update_attributes(params[:blog])
  end
end

When we update and create a record, it will expire the action cache.  Here’s the corresponding log entry:

Expire fragment views/localhost:3000/blogs (59.0ms)

The expire_action method takes the same arguments as expire_page above:

expire_action(:controller => :blogs, :action => :show, :id => 25) 

Configuration

If we need to cache an action based on some conditions, we can do that by using :if or :unless with a proc. For example, if we don’t want to cache the index action for json requests:

class BlogsController > ApplicationController
  caches_action :index, :if => Proc.new{|c|!c.request.format.json?}
  def index
    @blogs = Blog.all
    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @blogs }
      format.csv
    end
  end
end

We can also configure the default cache path.

caches_action :show, :cache_path => { :project => 1 } 

If you pass :layout => false, it will only cache your action content. It is useful when your layout has dynamic information .

Fragment Caching

Page caching and action caching both cache an entire page or an entire action,  but what if we just need to cache a part of a view? This is where fragment caching comes into play. We usually have a view with dynamic content, so we can’t store the whole page.  However, it is likely that we can cache bits of the page and expire them as needed.

If we are showing a blog post with their comments, we can cache the comments by themselves:

<div>
  <%= @blog.title %><%= @blog.desc %>
</div>

<%cache("blog_comments_#{@blog.id}") do%>
  <h3>Comments</h3>
  <% @blog.comments.each_with_index do|c, i|%>
    <div><%= c.value%><span><%= c.created_at.strftime('%B %d %Y')%></span></div>
  <%end%>
<%end%>
<%= link_to 'Add New comment', new_blog_comment_path(@blog)%>

The comments section should be changed when adding a new comment, and we are caching them with the key “blog_comments_#{@blog.id}”). Here is what the log says:

Write fragment views/blog_comments_1

Expire Cache

To expire this cache, call the expire_fragment method . Here, it is called when a new comment is created:

class BlogsController < ApplicationController
  def create
    @blog = Blog.find params[:blog_id]
    expire_fragment("blog_comments_#{@blog.id}")
    @comment = @blog.comments.new(params[:comment])
  end
end

Configuration

If a key name is not provided to the cache function, then Rails will cache the block based on the name of the controller action. In this case, you would expire the cache by calling expire_fragment(:controller => ‘blogs’, :action => ‘show’).

This, obviously, will generate conflicts if

  • We also caching action
  • And  having  multiple fragment cache per action

We can add an action suffix to avoid such conflicts:

cache(:action => 'show', :controller => 'blogs', :action_suffix => 'blog_comments' )

To expire this cache:

expire_fragment(:controller => 'blogs', :action => 'show',:action_suffix => 'blog_comments')

Sweepers

Calling an expire method inside a controller action is a bad idea.  Rails provides another mechanism for this expiration process:  Sweepers. Sweepers are responsible expiring caches . They observe models and execute callbacks as defined in the sweeper.

For example I created a blog_sweeper.rb file:

class BlogSweeper < ActionController::Caching::Sweeper
  observe Blog
  def after_update(blog)
    expire_action(:controller => :blogs, :action => :index)
  end
  def after_create(blog)
    expire_action(:controller => :blogs, :action => :index)
  end
end

But we need to  call this sweeper in our controller for this call cache_sweeper method in controller

class BlogController < ApplicationController
  cache_sweeper :blog_sweeper, :only => [:index]
end

Cache Digests 

Cache digests are a better way to handle  fragment caching. It’s based on a Russian Doll  scheme, meaning, when you have nested cached fragments and the nested content changes, only expire the cache for that content reusing the rest of the cache. Here is an example from our same blog application. Our model structure look like :

class User < ActiveRecord::Base
  has_many :blogs
end
# Blog model
class Blog < ActiveRecord::Base
 has_many :comments
 belongs_to :user, :touch => true
end
#comment model
class Comment < ActiveRecord::Base
 belongs_to :blog, :touch => true
end

We are using the touch option here so if we update a comment, the blog will get a new updated_at value.

Here is the ‘users/show.html.erb’, showing user’s detail with their blog and comments:

<%cache @user do%>
  <div>  Name:<%= @user.name %></div>
  <div>  Email:<%= @user.email %></div>
  <%= render @user.blogs%>
<%end%>
<!-- 'blogs/blog.html.erb' -->
<%cache blog do%>
  <div><%= blog.title %></div>
  <div><%= blog.desc %></div>
  <%= render blog.comments%>
<%end%>
<!-- 'comments/comment.html.erb' -->
<%cache comment do%>
  <%= comment.value %>
<%end%>

You can see that we cache the comments inside the blog and the blog inside the user. In an example where we have a single user with 2 blog posts and 1 comment, the cache keys look like:

views/users/1-20130118063610
views/blogs/1-20130118063600
views/comments/1-20130118063600
views/blogs/2-20130118063610

It is a combination of view path, object id, and timestamp of the last time it was updated. This combination ensures a cache will always be expired if a model is updated. If we add a new blog or edit a blog it will expire the cache of user along with blog cache. because we have used touch option in our blog model.

Likewise, if we add or edit a comment, everything expires. However, if we change any thing in the view itself, such as a new style, reloading the page will not reflect the change as our cache keys are not expired. We can mitigate this by keeping a version of the cache keys and changing that version as needed.

<%cache ['v1', @user] do%>
  <div>Name:<%= @user.name %></div>
  <div>Email:<%= @user.email %></div>
  <%= render @user.blogs%>
<%end%>
<!-- 'blogs/blog.html.erb' -->
<%cache ['v2',blog] do%>
  <div><%= blog.title%></div>
  <div><%= blog.desc %></div>
  <%= render blog.comments%>
<%end%>
<!-- 'comments/comment.html.erb' -->
<%cache ['v3',comment] do%>
  <%= comment.value %>
<%end%>

Every time we modify our view, we need to update the version of the cache key. Now we have more problems:

  • We need to remember the version number of the template
  • Parent template versions should be changed if child template is modified.
  • If the same partial is used in different  views, then it hard to maintain versions

Don’t panic!  Rails always tries to make thing simpler. In Rails 4, cache_digests have been added to maintain nested fragment caching.

With Cache Digests, we actually don’t need to worry about expiring caching and versioning  when we have nested fragment caching.  By the way, you can use cache digests in Rails 3.X by adding the cache_digests gem to your Gemfile.

<%cache @user do%>
  Name:<%= @user.name %>
  Email:<%= @user.email %>
  <%= render @user.blogs%>
<%end%>
<!-- 'blogs/blog.html.erb' -->
<%cache blog do%>
  <%= blog.title %>
  <%= blog.desc %>
  <%= render blog.comments%>
<%end%>
<!-- 'comments/comment.html.erb' -->
<%cache comment do%>
  <%= comment.value %>
<%end%>

Rendering the user’s show it will generate a log like:
cache_digest1

The cache key  for users/show is suffixed with an MD5 of the template itself and all its dependencies. If  we change anything in any view(users/show, _blog or _comment) and restart the server, it will automatically expire the cache key and recompute a new cache key.

In production, we don’t need to worry because every time we deploy our application it restarts the server.

Check Dependencies of Template

To check the dependencies of a view, the cache_digests gem provides two rake tasks

1. rake cache_digests:dependencies TEMPLATE=users/show 

I got the following output by running the above task

dependency_task

It gives you the dependencies of the user/show view only.

2. rake cache_digests:nested_dependencies TEMPLATE=users/show  which gives the output

nested_dependencies_task

It gives you the dependencies of the user/show view along with all its nested views .

Others way to call dependencies:

1.  We can call a method in render mentioning  the partial name and collection explicitly. For example:

<%= render @user.last_one_month_blogs%>

So we need to mention partial name and collection here

<%= render :partial => 'blogs/_blogs', :collection => @user.last_one_month_blogs%>

2. If we are using a helper in our render call. Then we need to use a comment that will specify the template dependency

<%# Template Dependency: comments/comment %>
<%= render_comments(blog)%>

Wrap Up

Basically, I wanted to explain caching in Rails and how many ways caching can be accomplished.

More Resources -:

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.