Did you know that you can navigate the posts by swiping left and right?

Rails Caching Again

04 May 2014 . category: rails . Comments
#ruby #rails #caching

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!

Do you want to shape up your Rails security skills?

Although Rails is quite secure by default, you can still easily shoot yourself in the leg, make silly mistakes and get hacked. I am working on a course, in which I will show you how an attacker would try to hack a Rails application and the best techniques to prevent it.