Rails Caching Again

04 May 2014 By: Greg Molnar

In the previous post I covered how can you use Rails' Russian Doll caching to make you app super fast. I didn't cover though how to cache search result pages and paginated results, so here comes the second part of that article.

I made a sample application where I have a product listing page with pagination and a search form: https://github.com/gregmolnar/rails-caching

Caching of the individual products is simple:

# app/views/products/index.html.erb
<% cache(product) do %>
  <tr>
    <td><%= product.name %></td>
    <td><%= product.price %></td>
    <td><%= link_to 'Edit', edit_product_path(product) %></td>
    <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>

But we want to cache the full list too so we need to generate a cache key by ourself. My solution to this problem is to pluck the ids, join them and add the max updated_at value to the end of the string:

# app/helpers/products_helper.rb
module ProductsHelper
  def cache_key_for_products(products)
    ids = products.pluck(:id).join('-')
    max_updated_at = products.pluck(:updated_at).max
    "products/#{ids}-#{max_updated_at.to_i}"
  end
end

Now we can cache a bigger fragment in the view:

# app/views/products/index.html.erb
<%= cache(cache_key_for_products(@products)) do %>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Price</th>
        <th colspan="2"></th>
      </tr>
    </thead>

    <tbody>
      <% @products.each do |product| %>
        <% cache(product) do %>
          <tr>
            <td><%= product.name %></td>
            <td><%= product.price %></td>
            <td><%= link_to 'Edit', edit_product_path(product) %></td>
            <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
          </tr>
        <% end %>
      <% end %>
    </tbody>
  </table>
<% end %>

This methods works for pagination and sorting too, since it relies on the order of the ids. If there is a search functionality too, all we have to do is to pass a suffix to the helper method:

# app/helpers/products_helper.rb
module ProductsHelper
  def cache_key_for_products(products, suffix = '')
    ids = products.pluck(:id).join('-')
    max_updated_at = products.pluck(:updated_at).max
    "products/#{ids}-#{max_updated_at.to_i}#{suffix}"
  end
end

I would pass the attribute name and the value so if someone is searching for a product name 'Jewel':

cache_key_for_products(@products, "jewel=#{@search.jewel_eq}")

That's it for now. I hope you enjoyed the article.

Update

David Patrick(dponrails) did some benchmarks and it turned out pluck is pretty slow so here is a better performing alternative would be the usage of map:

# app/helpers/products_helper.rb
module ProductsHelper
  def cache_key_for_products(products, suffix = '')
    ids = products.map(&:id).join('-')
    max_updated_at = products.map(&id).max
    "products/#{ids}-#{max_updated_at.to_i}#{suffix}"
  end
end

Thanks David for the heads up!

PS: If you want to get updates from me please subscribe to my email list.
I hate spam as much as you do, so I won't send you anything else than Ruby/Rails related updates occasionally, and of course you can unsubcribe anytime.