Fragment Caching in Rails - Part 2

 
avatar

In my last post, I talked about fragment caching in Rails in terms of caching entire actions from a controller, or what is called Action Caching. Obviously, action caching isn't always going to work because it again caches the entire output of an action, much like Page Caching. The only difference is that with Action Caching we were able to run before and after filters with Rails to ensure that we could do things like authentication.

However, one of things that we want to accomplish is to be able to cache with more control. For example, let's say that once a user is logged into our site, we want them to see a list of articles, but also have a personalized message at the top, something like "Welcome Back, Brian". Or, the content of the page would be adjusted to provide content that is specific to the user's preferences, meaning that the results of the same action can be different depending on the user that is making the request. In this case, we need to look at something called Fragment Caching.

I know, it's a little confusing. However, if we look at Fragment Caching as a topic, and within that topic there are two types to explore, one would be Action Caching, and the other would be Fragment Caching. The argument can be made that Action Caching is not really a type of Fragment Caching, but rather it's own type. That could be true, and to be honest I don't really care. Semantics have never helped me to complete a project faster or more effectively, so I don't really care about them. I have grouped Action Caching and Fragment Caching together because they both cache data to the same place, and are both ways to let Rails get involved rather than simply serving page caches. So, for that reason I have chosen to group them, hopefully that is the end of that.

On to more important things, such as what is a Fragment Cache? In the past few examples we have been creating our caches by inserting code into our controllers. With Fragment Caching we will move into the views of the pages that we want to cache, since we are going to look at caching specific parts of a page. To illustrate this, let's take a look at a typical view file for our action "articles", which will list out articles to our users:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
   <head>
       <title>Sample View File</title>
    </head>
    <body>
        <% for article in @articles %>
        <h1><%= article.title %></h1>
        <p><%= article.content %></p>
        <%= link_to "Read Article", :action => :view_article,
                                                 :id => article %>
        <hr />
         <% end %>
    </body>
</html>

As you can see, when a user visits the articles action, we have a list of articles being output. Each article title is presented in an H1 tag, and then a content summary is output in a paragraph tag, and then a link is output so that the user can read the entire article. Finally, a horizontal rule separates each article from the one below it.

This is a pretty simple action and view, but we want to change it a bit so that the articles are tailored to our users preferences. I'm not going into the details of that, since this is about caching. However, in our controller we would then grab the users ID from session data (since they need to be logged in) and then tailor our database query to that user, resulting in the same view file but the resource @articles would then contain a different set of articles. Precisely why Page or Action Caching will not work for us in this example. Imagine if I visited the site, logged in, and viewed a list of articles tailored to me. With Page or Action caching, a cache would be generated for the URL /articles, since that is the URL that I requested. Now the next time someone else logs in and visits that URL, they would be given a list of articles tailored to me, not them. This is because that is the content that was cached for that URL, and we don't want that to happen at all. Enter the Fragment Cache.

Take a look at the two lines of code that I need to add in order to make this happen. I'll go over what they do in a minute:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
  <head>
    <title>Sample View File</title>
  </head>
  <body>
    <% cache (:controller => "articles",
                :action => "index",
                :id => session[:user_id]) do %>
    <% for article in @articles %>
    <h1><%= article.title %></h1>
    <p><%= article.content %></p>
    <%= link_to "Read Article", :action => :view_article,
                                :id => article %>
    <hr />
    <% end %>
   **<% end %>**
  </body>
</html>

As you can see, we simply wrapped the output block with the following Ruby code:

<% cache (:controller => "articles",
       :action => "index",
       :id => session[:user_id]) do %>
<% end %>

And our work on the view file has been completed. However, we still need to make some changes in our controller. Rather than query the database each time, we need to tell our controller that we are caching information. In this case, rather than being as all-inclusive as Page and Action caching, we can make these decisions on a per-query basis. So, in our example, we want to query the database if there is no cache for a particular user's list of articles, which would translate to this in our articles controller:

def index unless read_fragment(:controller => :articles, :action => :index, :id => session[user_id]) @articles = Article.findbyid(session[user_id]) end end

What we have done here is say to the controller that when someone accesses the url /articles*, we want to grab their userid from the session data (they are logged in) and find the articles with their ID associated with it. This is extremely simplified as a caching example, as this would not be how you do this. We are then given the resource @articles which holds the results that are specific to this user. However, there is now the "unless readfragment" line, which tells Rails to check first if there is a cache file with the correct name. If so, it will use that. If not, it will query the database and generate the cache file. Remember, for a user ID of 23, the cache file in this case would be create at /articles/index/23, which matches the naming convention that we used in our view file.

If you've been following along in these posts, you probably already know what is coming next. We have to expire these caches as well, and we have the following controller code to do it when we want to:

expire_fragment(:controller => :articles, 
                         :action => :index, 
                         :id => session[user_id])

Again, as long as the name of the cache matches the controller code that you are using, things will be fine. This will remove the cache file so that next time that page is requested the cache will be rebuilt. Again, I'll talk more about expiring caches in the next few posts.

For the most part that is it. Just to be clear, you do not always have to name your cache files. Rails will fill in the gaps and do a pretty good job for simple application, but I highly recommend getting in the habit of naming each fragment cache as we did. Additionally, when creating name, they do not have to be real. For example, the controller, action and any other parameters that you use to name your cache files do not really need to exist. They can be totally fake, in fact. The only thing that matters it that you use the same names in your controller to read the fragments. As long as the controller is looking for the same fragment name that the view is creating, everything will work fine.

Category: Developers' Corner | Tags: Ruby on Rails

0 Comments

Rss-sm

Sign-up to receive EcommerceNotes, our acclaimed email newsletter.

View A Sample | Privacy

Connect with us

Bloggers Wanted

We’re looking for merchants and other ecommerce professionals to share their experiences with our readers. If this interests you, we invite you to contact us.

Help

Featured Tags | All A-Z

 

Inside Practical eCommerce