172: Touch and Cache
(view original Railscast)
Rails has recently been updated to version 2.3.3. This is a minor update and most of it consists of bug fixes, but there are a few new features too. One of these is called touch
, and in this episode we’ll show you how to use it to improve your application’s caching.
If you haven’t upgraded to version 2.3.3 yet you can run
sudo gem update rails
from the command line to install the new version. Then in any applications you want to update, you can change the version number listed at the top of /config/environment.rb
.
# Specifies gem version of Rails to use when vendor/rails is not present RAILS_GEM_VERSION = '2.3.3' unless defined? RAILS_GEM_VERSION
If you’re upgrading an application make sure that it has a good test suite so that you can be sure that upgrading hasn’t broken anything.
Using touch With Fragment Caching
The article page for a blog application is shown below. This page gets a lot of traffic so we want to improve its performance.
One way we could improve the response time is to add fragment caching to the page. This might not be the ideal solution, but it’s a quick-and-dirty fix for the problem. Let’s implement fragment caching on the page and see how we get on.
Caching is disabled by default in development mode so first we’ll have to turn it on. We do this in /config/environments/default.rb
.
config.action_controller.perform_caching = true
An alternative to this is would be set up a staging environment. If you need more information about how to do that it was covered back in episode 72. Setting up a staging environment removes the need to enable and disable caching in your development environment.
The fragment caching will be added to the articles’ show
view. The view code looks like this.
<% title @article.name %> <p class="author"><em>from <%=h @article.author_name %></em></p> <%= simple_format @article.content %> <p><%= link_to "Back to Articles", articles_path %></p> <% unless @article.comments.empty? %> <h2><%= pluralize(@article.comments.size, 'comment') %></h2> <div id="comments"> <% for comment in @article.comments %> <div id="comment"> <strong><%= link_to_unless comment.site_url.blank?, h(comment.author_name), h(comment.site_url) %></strong> <em>on <%= comment.created_at.strftime('%b %d, %Y at %H:%M') %></em> <%= simple_format comment.content %> </div> <% end %> </div> <% end %> <h3>Add your comment:</h3> <%= render :partial => 'comments/form' %>
To add fragment caching we call cache
and wrap the part of the page we want to cache in its block.
<% title @article.name %> <% cache @article do %> <p class="author"><em>from <%=h @article.author_name %></em></p> <!-- Rest of code omitted --> <% end %> <h3>Add your comment:</h3> <%= render :partial => 'comments/form' %>
The cache
method takes an optional argument and the argument’s value is used as the key that the cache is stored under (by default the page’s URL is used). If we pass a model, then its cache_key
is used. This means that the item will be expired automatically when the article is updated. We can demonstrate this in the console.
>> a = Article.first => #<Article id: 1, name: "The Piano, a Marvellous Instrument", content: "The piano is a musical instrument played by means o...", author_name: "Billy Belmer", created_at: "2009-06-14 19:39:40", updated_at: "2009-07-29 19:14:17"> >> a.cache_key => "articles/1-20090729191417"
The cache_key
is a string made up of the name of the model, and its id
and updated_at
attributes. The last part of the key is very useful as it means that the key changes every time the model is updated. Therefore the item is expired from the cache when any of the model’s attributes change.
We can show the cache in action by refreshing our article’s page twice and looking at the development log.
Processing ArticlesController#show (for 127.0.0.1 at 2009-07-30 20:22:30) [GET] Parameters: {"id"=>"1"} Article Load (0.2ms) SELECT * FROM "articles" WHERE ("articles"."id" = 1) Rendering /Users/eifion/rails/apps_for_asciicasts/ep172/app/views/articles/show.html.erb Cached fragment hit: views/articles/1-20090729225258 (0.0ms) SQL (0.2ms) SELECT count(*) AS count_all FROM "comments" WHERE ("comments".article_id = 1) CACHE (0.0ms) SELECT count(*) AS count_all FROM "comments" WHERE ("comments".article_id = 1) Comment Load (0.3ms) SELECT * FROM "comments" WHERE ("comments".article_id = 1) Cached fragment miss: views/articles/1-20090729225258 (0.0ms) Processing ArticlesController#show (for 127.0.0.1 at 2009-07-30 20:22:45) [GET] Parameters: {"id"=>"1"} Article Load (0.2ms) SELECT * FROM "articles" WHERE ("articles"."id" = 1) Rendering /Users/eifion/rails/apps_for_asciicasts/ep172/app/views/articles/show.html.erb Cached fragment hit: views/articles/1-20090729225258 (0.0ms)
The first time the page reloaded there was a cached fragment miss as no item matching the key was found. The data was therefore pulled from the database and the cache item created. The second time the cached item is found so we have a cached fragment hit and there is no need to get the comments the database.
If we edit the article, say by changing the title, then the cache item will automatically be expired and the page will be updated. This is because a new cache_key
is created when we update the article based on the its changed updated_at attribute.
Using Touch
All the work we’ve done so far would work in previous versions of Rails, so how does touch
fit in? We’ll begin by showing how touch
works in the console.
We’ll get the first Article and find its updated_at attribute.
>> a = Article.first => #<Article id: 1, name: "The Piano, a Beautiful Instrument.", content: "The piano is a musical instrument played by means o...", author_name: "Billy Belmer", created_at: "2009-06-14 19:39:40", updated_at: "2009-07-29 20:27:34"> >> a.updated_at => Wed, 29 Jul 2009 22:27:34 UTC +00:00
If we call touch
on the article its updated_at
attribute will be changed.
>> a.touch => true >> a.updated_at => Wed, 29 Jul 2009 22:27:53 UTC +00:00
This is basically all touch
does. When called on a model it changes its updated_at
attribute to the current time. This doesn’t seem like a particularly useful method to have, but it comes into its own when dealing with associations.
In our application an Article
can have many Comments
. If we use the form on an article’s page to add a comment to that article, the comment won’t be shown as the part of the page that shows the comments is cached. The article’s timestamp is not changed when a comment is added so the cache item won’t be expired.
All we have to do to ensure that the article is touched when a comment is added or changed is make a small change to the Comment
model.
class Comment < ActiveRecord::Base belongs_to :article, :touch => true end
Adding :touch => true
to the belongs_to
relationship means that when a comment is created, updated or destroyed the article it belongs to is touch
ed. Now when we add a third comment the cache will be expired and the page updated to show the new comment.
This technique can be used outside of fragment caching. This works well, for example with Memcached. Memcached will automatically roll off the old caches as new ones are created. There is no need to use sweepers and to have to deal with the extra code that they require.