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.

Eg

    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.

Eg

    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 profile action.

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 expire_fragment()

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.

Eg:

    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

Here the index action in home_controller.rb will render the home page if the users aren’t signed in.

Blog Home page for unsigned users

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).

Post sign up, users are redirected to articles list page

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.

app/controllers/home_controller.rb

    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.

Also note 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

Gotcha

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

1. Sign up 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 :) .

Interested in contributing similar blog on Codelearn that reaches 10k users, contact pocha@codelearn.org . For a limited time, we are offering $100/blog post.