This blog explains Rails Caching techniques followed by an example app that uses those techniques in a sample blog application. This blog assumes basic Rails knowledge (MVC architecture of Rails including Model Views & Controllers). You may try Rails Tutorial at Codelearn if you are just starting with Rails before proceeding with this article.
Also, you can try out the example app (mentioned later in this blog), on Codelearn Playground by cloning it from github directly onto Codelearn Playground.
Rails Cache Techniques
There are three types of Rails caching techniques in Rails 3:
- Page Caching
- Action Caching
- Fragment Caching
Note Rails caching is disabled by default in the development environment. Make sure you have below the parameter value below set to true in your Rails app config file.
#Inside config/environments/development.rb config.action_controller.perform_caching = true
Rails caching is enabled in production mode by default.
Rails Page Caching
In Rails Page Caching, whenever a request is sent to the server, the Rails server would check for the cached page and if that exists it would be served. If it does not exist, Rails server generates the page & cache it. Hence the Rails app won’t have to generate it again during the next request.
Class UserController < ActionController caches_page :profile def profile @user = current_user end end
To expire the cache when an update is made, we will have to call an
expire_page helper method.
Class UserController < ActionController caches_page :profile def profile @user = current_user end def update expire_page :action => profile end end
Here it is assumed that
update action is being called when the page is updated.
expire_page inside the action makes sure the cached page is purged & new cached page is created during the next call to
This type of caching in Rails is lightning fast, but one main disadvantage of this is that this can’t be used for caching every page. As the requests don’t go to the Rails app, the authentication and access restrictions using
before_filter won’t work if page caching is used.
The above example is probably the wrong usage of the Rails page cache. You can see that page served by ‘profile’ action has dependency on the current_user (assume it to be logged in user).
Let’s say user_1 is the first user to view the page. The page will be generated & cached with contents for user_1 . If user_2 tries to go to his profile page, he will see user_1 content on it. This is plain wrong .
Rails Action Caching
While Rails Page caching caches the complete page (& the request never reaches the Rails controller), Rails action caching only caches the activities happening inside the action. For e.g., if a Rails action is fetching data from database & then rendering a view, these items would be cached & directly used the next time the cache is accessed.
In Rails Action Caching, the disadvantages of Rails Page Caching won’t be a problem as all the requests will be sent to the appropriate Rails action. Hence the authentication and access restrictions using the before_filters can be applied before serving a page.
The code for Rails Action Caching is similar to Rails Page Caching
Class UserController<ActionController before_filter :authenticate caches_action :profile def profile @user = current_user end def update expire_action :action => profile end end
Rails Fragment Caching
Rails Fragment Caching is mainly used for dynamic pages. In this type of caching, fragments of a page can be cached and expired.
Consider an example in which an article is posted to a blog and a reader wants to post a comment to it. Since the article is the same this can be cached while the comments will always be fetched from the database. In this case we can use Rails Fragment Caching for article.
P.S. – Rails Fragment Caching is best done in the Views. The code snippet below is part of Rails View unlike previous examples where code snippets are part of Rails Controllers.
<% cache("article") do %> <%= render article %> <% end %> <% @article.comments.each do |comments| %> <%= comments.user_name %> <%= comments.user_comment %> <% end %>
The highlighted code will cache whatever is between `cache(‘article’) do … end`. The name `article` is used to reference the fragment cache block.
The cached fragments can be expired by using
Class UserController < ActionController def profile @user = current_user end def update expire_fragment("article") end end
Rails SQL Caching
SQL Caching will cache any SQL results performed by the Active Records or Data mappers automatically in each action . So, the same query doesn’t hit the database again – thereby decreasing the load time.
Class ArticleController < ActionController def index @artilces = Article.all # Run the same query again @articles = Article.all # will pull the data from the memory and not from DB end end
This caching scope includes only the action in which the query is executed. In the above example, executing
Article.all the second time will used cached result. But outside the action, the same query will hit the database.
Rails cache explained with an example blog app
To demonstrate the concepts of Rails cache, I have made a simple blog app. You can view the app source code on github
index action in
home_controller.rb will render the home page if the users aren’t signed in.
Post signup/signin, the users will be redirected to http://localhost:3000/articles which is served by the
index action of articles controller. It is the list of blog posts (hereby referred as articles).
To perform caching of the home page, I applied Rails page caching to the index page.
The index function is redirecting users to articles page if the user is signed in.
Class HomeController < ApplicationController caches_page :index def index redirect_to articles_path if user_signed_in? end end
articles_path is linked to the http://localhost:3000/articles (screenshot above). The page is served by ‘index’ action in articles_controller.rb. The ‘Show’ link maps to ‘show’ action in articles_controller.rb .
Below is the screenshot of one article served by ‘show’ action. The URL will look like http://localhost:3000/articles/__article_id__
To cache index & show action in articles_controller.rb, I used Rails action caching. Since the actions are available only to signed in users, I could not use page caching here
Class ArticlesController < ApplicationController before_filter :authenticate_user! caches_action :index, :show def index @articles = Article.all end def show @article = Article.find(params[:id]) end end
We need to make sure that the rails cache is expired whenever an article changes. So we also need to add
expire_action to actions in articles_controller.rb
Class ArticlesController < ApplicationController before_filter :authenticate_user! caches_action :index, :show def index @articles = Article.all end def show @article = Article.find(params[:id]) end def create @article = Article.new(params[:article]) if @article.save expire_action action:[:index,:show] #expire the cache whenever a new article is posted end end end
Now the article list page (http://localhost:3000/articles) where the user lands after sign in as well as every article page is cached using Rails action caching. The home page is cached using Rails page caching.
How to know if cache is working (or getting hit)
Lets take the example of the articles page (http://localhost:3000/articles)
If no cache is hit (or did not deploy any caching methods), you will typically see dump containing details of the url hit, database queried, view file fetched followed by the css & js calls in your Rails server log
Started GET "/articles" for 127.0.0.1 at 2013-06-03 15:56:35 +0530 Processing by ArticlesController#index as HTML User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Article Load (0.1ms) SELECT "articles".* FROM "articles" Rendered articles/index.html.erb within layouts/application (11.7ms) Completed 200 OK in 385ms (Views: 232.6ms | ActiveRecord: 4.5ms) Started GET "/assets/application.css?body=1" for 127.0.0.1 at 2013-06-03 15:56:36 +0530 Served asset /application.css - 304 Not Modified (100ms)
When I used action caching for the url, there is no querying articles in the database & reading the view file
Started GET "/articles" for 127.0.0.1 at 2013-06-03 14:13:30 +0530 Processing by ArticlesController#index as HTML User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Read fragment views/localhost:3000/articles (0.2ms) Completed 200 OK in 2ms (ActiveRecord: 0.3ms) [2013-06-03 14:13:30] WARN Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true Started GET "/assets/application.css?body=1" for 127.0.0.1 at 2013-06-03 14:13:30 +0530 Served asset /application.css - 304 Not Modified (19ms)
Querying db happened for the users, which means the before_filter & authentication is being honored.
Read fragment views/localhost:3000/articles line which suggests Rails cached the action & served.
Note the performance improvement. Without cache, the time to serve the page was 385 ms ‘Completed 200 OK in 385ms (Views: 232.6ms | ActiveRecord: 4.5ms)’
The time to serve the page came down to 2ms. I am tempted to calculate the percent change, but I know that does not serve any purpose ‘Completed 200 OK in 2ms (ActiveRecord: 0.3ms)‘
Do note that js & css calls happen as they are. So in your log, you will see lots of these entries. Make sure you do not overlook the important ones highlighted above.
In Rails page caching, the app request does not enter the Rails app. Your Rails app log should be nearly empty. I say nearly because I see only the below line in my Rails server development log while rendering http://localhost:3000/ (home page)
[2013-06-03 14:13:20] WARN Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true
It all looked good till I tried accessing http://localhost:3000/ (home page) again . I was signed in & as per the action logic, I should have been redirected to http://localhost:3000/articles page .
But it does not happen – I still see the home page & clearly the flow is broken. Ok I know, its the Rails Page caching.
Going back to the code, I realized what I have done wrong. Post sign in/Signup, I am explicitly redirected to articles_path (http://localhost:3000/articles) . The control does not come back to the index action of home controller. Hence I do not see this issue post sign up.
Clearly, this thing can be fixed – but I would leave that to the readers. Suggest your fix (possibly fork the repo & provide a link to your app for people to view your solution on github) in the comments.
Do not have Rails installed on your PC ? Fret not. You can try the app on Codelearn Playground for free.
Try the example blog app at Codelearn
2. Clone repo from github
#Execute in Terminal git clone https://github.com/codelearn-org/rails-cache-example-app
You will see a directory with name
caching_demo_app in your home directory.
3. Get inside the directory
#Execute in Terminal cd caching_demo_app
4. Install the gems
#Execute in Terminal bundle install
5. Migrate the database
#Execute in Terminal rake db:migrate
6. Update development environment file. Search for it in File Browser, clicking the file will open it in Code Editor. Change the indicated value below from false to true.
#Update config/environments/development.rb config.action_controller.perform_caching = false true
7. Run the server
#Execute in Terminal rails s
8. Refresh App Output tab & start exploring. Check the home page, sign yourself up (use any email or password, don’t worry it does not go outside your app ) and add new articles . Check Rails server logs to verify the Rails cache in action.
Update - Rails 4 has pushed Page & Action caching to different gems & they are not part of the default Rails app. Instead a new form of caching called Russian Doll Caching is introduced. It is an advanced version of Fragment Caching.
If you find this post informative & want me to cover Russian Doll caching in the next post, I am listening to the comments .