Shopify/identity_cache — pull request #18 by hornairs (2013-03-28 03:59:52+00:00)
Title: Requested includes

@burke @camilo for review, @boourns @jstorimer for sensibility check please!

Lets say we are loading a bunch of orders. Orders cache their line items:

```ruby
class Order < ActiveRecord::Base
include IdentityCache

cache_has_many :line_items, :embed => true
```

and lets say those line items reference a product variant they need to ask some stuff to serialize themselves:

```ruby
class LineItem < ActiveRecord::Base
include IdentityCache

cache_belongs_to :product_variant, :embed => false
```

When we `fetch_multi` a bunch of orders, they are smart enough to know that for all the IDC cache misses, we should do one batch select for the missed order rows instead of n SELECTs for each missed order. IDC is also smart enough to use Arel's `includes` to add all the embedded cached associations to that batch select so that as it populates the cache for each missed order it can populate the association caches without causing a different database n+1. It even does this all the way down the nested tree for embedded associations.

This N+1 prevention strategy breaks down when we start asking for `:embed => false` associations from the `fetch_multi`'d records or their children. If you recall, `:embed => false` associations are implemented as pretty simple methods on top of the associated model's `fetch` method as opposed to involving any blobs that go into the cache with the parent or anything. This means that if we do `LineItem.fetch_multi(1, 2, 3).map(&:fetch_product_variant)`, we get N `ProductVariant.fetch` calls. Those guys don't happen with any special scope or anything which would let them batch the memcached or DB calls, so they end up doing N memcache calls, and in the worst case where each one misses, N DB calls.

This is part one of my solution to this problem. By adding an `options` parameter to the fetch_multi, we can ask that the batch select process that happens during `fetch_multi` tack on some extra `includes` for the database call. With that param we can ask for more associations we know we are going to need and prevent DB N+1s. The use case for this is serialization: if can we know what non-embedded associations we need before the `fetch_multi` call, we can batch fetch them from the DB for any records which miss the json layer and the IDC layer. For example, we know which associations we want from the `order_api_includes` method which feeds the `includes` call in the Orders controller, which would include the `product_variant` for the `line_item` above.

Now, some responsible disclosure. This is only step one of a pretty big change. First, when we provide requested includes to the `fetch_multi` method, they are always asked for from the database on the list of top level IDC cache misses. Usually, the list of includes for the DB find includes only things we are going to need to populate the embedded associations for the IDC blob. These things, since they are going in the parent blob, are by definition not already in the cache, so we need them. By including non-embedded associations in the list of things to find upon miss, the invariant of "all the stuff i'm about to find isn't yet in the cache" goes away. IDC will add this extra list of associations and in the example above select all the product variants for the line items, regardless of weather or not those product variants are already in the cache. Note that for "excessive" product variant fetching to happen, the order has to be an IDC cache miss, which means the line_items will be selected and thus the product variants selected, and the product variant has to already be in the cache. Seems rather likely. For the time being this tradeoff of finding N truly cache-missed variants vs always having one find for many probably missed variants is hard to evaluate, but read on for the reasoning.

IDC does not (yet) have the smarts to traverse a tree of associations from the top and `fetch_multi` the successive levels of the combined list of child IDs it knows it needs for each parent record. ActiveRecord does this through `includes`, if you ask for 5 orders and all their line items, it will issue one select for the orders and then select all the line items with `order_id IN` the list of orders from the first select. We could do the same thing while traversing the IDC association tree: `fetch_multi` the order blobs, look at all the embedded line items and grab all the `variant_id`s, and then `fetch_multi` the variants, which if any are missed will issue one efficient select for the misses in the normal `fetch_multi` style. Sounds pretty complicated. This PR is meant as a first step towards this goal, as we need some way to tell IDC we actually want those variants from the `line_items` for it to have the information it needs to `fetch_multi` them and eventually efficiently select the misses.

Thoughts?