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:
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
It gives you the dependencies of the user/show view only.
2. rake cache_digests:nested_dependencies TEMPLATE=users/show which gives the output
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 -: