ruby-on-railsruby-on-rails-3cachingrussian-doll-cachingcache-digests

Rails Russian Doll Caching and N+1


From what i understand of Russian doll caching in Rails it would be detrimental to eager load related objects or object lists when we are doing RDC (Russian Doll Caching) because in RDC we just load the top level object from the database, and look up its cached rendered template and serve. If we were to eager load related object lists that will be useless if the cache is not stale.

Is my understanding correct? If yes, how do we make sure that we eager load all the related objects on the very first call so as not to pay the cost of N+1 queries during the very first load (when the cache is not warm).


Solution

  • Correct - when loading a collection or a complicated object with many associations, a costly call to eager load all objects and associations can be avoided by doing a fast, simple call.

    The rails guide for caching does have a good example, however, it's split up a bit. Looking at the common use case of caching a collection (ie the index action in Rails):

    <% cache("products/all-#{Product.maximum(:updated_at).try(:to_i)}") do %>
      All available products:
      <% Product.all.each do |p| %>
        <% cache(p) do %>
          <%= link_to p.name, product_url(p) %>
        <% end %>
      <% end %>
    <% end %>
    

    This (condensed) example does 1 simple DB call Product.maximum(:updated_at) to avoid doing a much more expensive call Product.all.

    For a cold cache (the second question), it is important to avoid N+1's by eager-loading associated objects. However, we know we need to do this expensive call because the first cache read for the collection missed. In Rails, this is typically done using includes. If a Product belongs to many Orders, then something like:

    <% cache("products/all-#{Product.maximum(:updated_at).try(:to_i)}") do %>
      All available products:
      <% Product.includes(:orders).all.each do |p| %>
        <% cache(p) do %>
          <%= link_to p.name, product_url(p) %>
          Bought at:
          <ul>
            <% p.orders.each do |o| %>
              <li><%= o.created_at.to_s %></li>
            <% end %>
          </ul>
        <% end %>
      <% end %>
    <% end %>
    

    In the cold cache case we still do a cache read for collection and each member, however, in the partially warm cache case, we will skip rendering for a portion of the members. Note that this strategy relies on a Products associations being correctly set up to touch when associated objects are updated.

    Update: This blog post describes a more complex pattern to further optimize building responses for partially cached collections. Instead of rebuilding the entire collection, it bulk fetches all available cached values then does a bulk query for the remaining values (and updates the cache). This is helpful in a couple of ways: the bulk cache read is faster than N+1 cache reads and the bulk query to the DB to build the cache is also smaller.