homeASCIIcasts

387: Cache Digests 

(view original Railscast)

Other translations: Ja

Below is a screenshot from an example application that shows a list of projects each of which has a number of tasks. Let’s say that we’re having performance issues on this page and that we want to add fragment caching to it to speed it up.

Our projects application.

The template for this page is shown below. This simply renders out the projects.

/app/views/projects/index.html.erb

Projects

<%= render @projects %>

For each project the _project partial is rendered. This template is also fairly simple and renders the tasks for each project.

/app/views/projects/_project.html.erb

<%= link_to project.name, edit_project_path(project) %>

This partial renders the _task partial for each task that the project has.

/app/views/tasks/_task.html.erb

  • <%= task.name %> <%= link_to "edit", edit_task_path(task) %>
  • We’ll add fragment caching to the _projects partial. If you’re not familiar with fragment caching take a look at episode 90 which covers it in more detail.

    /app/views/projects/_project.html.erb

    <% cache project do %>
      

    <%= link_to project.name, edit_project_path(project) %>

    <% end %>

    As we’re rendering out the tasks inside this partial it would be good if this cache expired when a task is changed. We can do this by modifying the Task model and adding touch: true to the project association.

    /app/models/task.rb

    class Task < ActiveRecord::Base
      attr_accessible :name, :completed_at
      belongs_to :project, touch: true
    end

    Now the project that a task belongs to will be marked as updated when that task is updated. So that we can see the effects of this we’ll modify the development configuration file and set perform_caching to true so that we can try this out.

    /config/development.rb

    config.action_controller.perform_caching = true

    When we restart our Rails app now and then reload the home page each of the projects will be stored in a fragment cache. Each subsequent time we reload the page those parts will be read from the cache. If we edit a task the cache for that task’s project will be expired and a new one will be created.

    Handling Changes to Cached Templates

    This works well but what happens if we make a change to the view template? Let’s say that we want to render the tasks in an ordered list instead of an unordered list.

    /app/views/projects/_project.html.erb

    <% cache project do %>
      

    <%= link_to project.name, edit_project_path(project) %>

      <%= render project.tasks %>
    <% end %>

    When we reload the page we won’t see any change. Each task’s HTML is already stored in the cache and as the cache hasn’t expired the old content is still shown. A common workaround for this problem is to add a version number to the cache key.

    /app/views/projects/_project.html.erb

    <% cache ['v1', project] do %>
      

    <%= link_to project.name, edit_project_path(project) %>

      <%= render project.tasks %>
    <% end %>

    Now because we’ve changed the cache key the cache will be expired and reloaded and we’ll see the tasks displayed in an ordered list.

    The changes we made are now picked up.

    Dealing With Nested Caches

    The problem with this approach is that every time we update the template we need to remember to change the version number so that the cache key changes and the changes are picked up. This isn’t difficult but things can get quickly out of hand if we have nested fragment caches. For example let’s say that we want to fragment-cache the tasks partial as well to give us a little extra performance.

    /app/views/tasks/_task.html.erb

    <% cache ['v1', task] do %>
    
  • <%= task.name %> <%= link_to "edit", edit_task_path(task) %>
  • <% end %>

    When we do this we’ll need to update the version number in the project partial’s cache key too so that the whole cache is expired. If we update the task partial now, say to change the link’s text from “edit” to “rename”, we’ll have to update the version number as well. We won’t see this change when we reload the page, however, unless we also change the version number in the projects partial as well. Once we’ve changed both the change will be shown.

    The changes to the tasks partial are now shown.

    Using The Cache Digest Gem

    Even though this has worked it’s annoying to have to remember to make this change every time we change a template and this is where the Cache Digests gem comes in useful. This functionality will be available by default in Rails 4 but has been extracted out into a gem so that we can use it in Rails 3 projects. This gem works by including a digest in the fragment cache keys that is based on the templates. This means that if the template changes a new cache key is generated and that cache will expire. We’ll try this gem in our application. As ever we use it by adding it to our gemfile then running bundle to install it.

    /Gemfile

    gem 'cache_digests'

    Now it’s no longer necessary for us to keep track of a version number in our fragment caches and we can remove them from both the project and task partials. When we restart our application now then reload its home page a digest will be included in the cache key that represents the template.

    When we make a change to the projects template now and reload the page the change doesn’t seem to be picked up. The reason for this is that the cache digest gem doesn’t read the template file on every request as this would be inefficient. Instead it keeps its own local cache of the digest for each template path in memory and this is why our template change hasn’t been picked up. To get this working in development we’ll need to restart our Rails app again. This normally isn’t a problem in production as when we deploy the new templates the application will be restarted anyway. When we reload the page now then changed template is picked up as the cache digest is now different.

    One good thing about this feature is that it’s smart enough to detect dependencies. For example it knows that the projects template has a render call for the projects tasks and so if the tasks partial changes it needs to expire the projects cache.

    That said we have to watch out for those cases where the dependency isn’t properly detected. For example we have an incomplete_tasks method on the Project model. If we use this in the project partial to render the incomplete tasks then change the tasks partial the change won’t be picked up as the dependency isn’t detected. It’s therefore a good idea to run a Rake task that the gem provides called cache_digests:nested_dependencies and pass it the path to the template.

    $ rake cache_digests:nested_dependencies TEMPLATE=projects/index
    [
      {
        "projects/project": [
          "incomplete_tasks/incomplete_task"
        ]
      }
    ]

    This tells us that it detected the project partial’s dependency, which is correct, but there is also an incomplete_task which isn’t correct, it should be task. This is something we need to change in order for the dependency to be picked up. In these cases its recommended to explicitly pass the partial option to render like this:

    /app/views/projects/_project.html.erb

    <% cache project do %>
      

    <%= link_to project.name, edit_project_path(project) %>

    <% end %>

    Now when we run the Rake task the tasks dependency will be properly detected and the cache digest will be updated properly.

    $ rake cache_digests:nested_dependencies TEMPLATE=projects/index
    [
      {
        "projects/project": [
          "tasks/task"
        ]
      }
    ]

    There are further details about this in the gem’s README which is worth taking the time to read. It tells us what different render calls will be parsed correctly and which ones won’t and shows us another way to specify a template dependency if we’re rendering a partial in a helper method.