Keys

image by Chunlea Ju

Ensure you correctly build your caching keys

Caching is a hugely powerful tool in maintaining the performance of often requested pages and partials in your application.

RailsConf 2024

I'm co-chairing RailsConf 2024 in Detroit May 7–9. Come and join us 

It can also cause confusing behaviour (mostly “stuff not updating on the page”) when you test your app in a production environment.

Rails has great built-in support for many types of caching, in particular its “view fragment caching”. This stores the resulting text of parts of your views in very fast storage, say Redis or Memcache, saving your application building the views every time a page is rendered.

The framework includes an elegant way of using the cache based on using a model’s id, its updated_at timestamp, and an automatically-generated digest of the specific view template. You can find more details in the Rails guide on caching.

Instead of…

…only using the main model in your cache key, for view fragments using multiple models:

class Event < ApplicationRecord
  has_many :attendees
end

class Attendee < ApplicationRecord
  belongs_to :event
  has_many :prerequistes
end

class Prerequistes < ApplicationRecord
  belongs_to :attendee

  scope :done, -> { where.not(completed_at: nil) }
end

app/views/events/show.html.erb

<% @event.attendees.each do |attendee| %>
  <% cache [attendee] do %>
    <p>
      <%= attendee.name %>, <%= @event.name %><br />
      <small>
        <%= attendee.prerequistes.done.count %> done
      </small>
    </p>
  <% end %>
<% end %>

Use

…multiple relevant models, including the parent object.

class Event < ApplicationRecord
  has_many :attendees
end

class Attendee < ApplicationRecord
  belongs_to :event, touch: true
  has_many :prerequistes
end

class Prerequistes < ApplicationRecord
  belongs_to :attendee, touch: true
  scope :done, -> { where.not(completed_at: nil) }
end

app/views/events/show.html.erb

<% @event.attendees.each do |attendee| %>
  <% cache [@event, attendee] do %>
    <p>
      <%= attendee.name %>, <%= @event.name %><br />
      <small>
        <%= attendee.prerequistes.done.count %> done
      </small>
    </p>
  <% end %>
<% end %>

Why?

This is to mitigate against a ‘powerful tools enabling subtle bugs’ problem.

When we use the data from any model in a view-cached partial, you need to include that model in the cache key, or the view will not update with changes to that model.

You also need to be aware of nested models. If a model has_many objects and you neglect to use touch: true in the declaration of the “child” model’s belongs_to relationship then, if there are changes to the child model, the cache key won’t be “busted”, and your view will show out-of-date information.

Why not?

It’s always worth checking whether you need caching at all. It is best to understand the performance improvements versus the possibility of maddeningly-hard-to-find bugs.

Caching bugs aren’t apparent in the normal places we look for bugs; the code, the logs, or in error reporting. Also your tests are unlikely to discover them as caching is typically turned off in development and test environments.

I’m definitely not saying “don’t cache”. I’m just saying “be very careful”.

Brighton Ruby 2024

Still running UK’s friendliest, Ruby event on Friday 28th June. Ice cream + Ruby 


Last updated on December 1st, 2019 by @andycroll

An email newsletter, with one Ruby/Rails technique delivered with a ‘why?’ and a ‘how?’ every two weeks. It’s deliberately brief, focussed & opinionated.