Caching Fragments

In order to speed up performance, sometimes you’ll want to cache a page. Once the data changes, the cached page should expire, in order to show the new information. Rails has some features built in to help us with this. One of which are Sweepers. The Rails API claims this:

Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change. They do this by being half-observers, half-filters and implementing callbacks for both roles.

[Rails API]] [http://api.rubyonrails.org/classes/ActionController/Caching/Sweeping.html] [ActionController::Caching::Sweeping]

However, much to my dismay, if an object was edited or created outside of the controller, the sweeper doesn’t seem to pick it up, despite the Rails documentation. This means cron jobs or objects created/edited in console doesn’t trigger the sweeper, and you’re left with stale cached pages. After digging through many stackoverflow threads, like this one, it seems like I wasn’t the only one to struggle with this.

However, I did find a workaround: fragment caching! Thanks to Ryan Bates for another great railscast.

Modify the configuration file and set perform_caching = true.

/config/environments/development.rb
1
config.action_controller.perform_caching = true

To cache a fragment of any view, simply wrap the code with <% cache 'something' do %> <% end %>. The fragment will be cached with whatever you want to call it. Let’s say you want automatic expiration. You can do this by replacing 'something' with something like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<p id="notice"><%= notice %></p>
<h3><%= @mixtape.name %></h3>
<table class="table table-bordered table-condensed table-hover">
  <tr>
    <th>Song Title</th>
    <th>Genre</th>
  </tr>
  <% @mixtape.songs.each do |song| %>
    <% cache song do %>
      <tr>
        <td><%= song.title %></td>
        <td><%= song.genre %></td>
      </tr>
    <% end %>
  <% end %>
</table>

Automatic expiration works here because of key-based expiration. Check it out by going to console and typing in song.cache_key.

1
2
3
4
5
6
7
8
9
001:0 > song = Song.first
  SCHEMA (3.4ms)  PRAGMA table_info("songs")
  SCHEMA (0.2ms)   SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence' AND name = "songs"
  SCHEMA (0.1ms)  PRAGMA table_info("songs")
[DEBUG]   Song Load (1.1ms)  SELECT "songs".* FROM "songs" LIMIT 1
  Song Load (1.1ms)  SELECT "songs".* FROM "songs" LIMIT 1
=> #<Song id: 1, title: "test", created_at: "2012-12-31 17:43:25", updated_at: "2013-01-02 10:37:58">
002:0 > song.cache_key
=> "songs/1-20130102103758"

The cache_key is updated through the updated_at timestamp. So the line <% cache song do %> checks to see if there is a fragment that is the same as song.cache_key. If not, it generates a new one, and expires the old one. More information about cache keys from DHH himself here.

One thing to be careful of is the naming of fragments. If you’re caching on multiple templates, chances are you might accidently create a fragment with the same name. To get around that, prepend a string by using an array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<p id="notice"><%= notice %></p>
<h3><%= @mixtape.name %></h3>
<table class="table table-bordered table-condensed table-hover">
  <tr>
    <th>Song Title</th>
    <th>Genre</th>
  </tr>
  <% @mixtape.songs.each do |song| %>
    <% cache ["available", song] do %>
      <tr>
        <td><%= song.title %></td>
        <td><%= song.genre %></td>
      </tr>
    <% end %>
  <% end %>
</table>

In our case, song belongs to a mixtape. So in the song.rb file I’ve added a touch: true to the association, to ensure the associated model, mixtape, gets updated everytime the song gets updated.

1
2
3
class Song < ActiveRecord::Base
  belongs_to :mixtape, touch: true
end

While you’re testing caching, you may notice your changes not going through. Be sure to Rails.cache.clear everytime you change the code regarding caching. Interesting? Read more about advanced caching here.

Comments