Space Vatican

Ramblings of a curious coder

Nested Includes and Joins

Nested eager loads are a not entirely obvious bit of rails syntax. It’s not hard once you get it though. In a nutshell you have to tell ActiveRecord how it should walk through the associations (i.e. just listing them isn’t enough). Before we get going it’s worth pointing out that although I’ve written :include everywhere, everything I’ve said applies equally to :include and :joins (see my previous post on the difference between the two).

When you are nesting includes, you’re building up a data structure that is inherently recursive. There are 3 rules for nested :includes: – Always use the association name. Not the table name, not the class name but the association name (whatever it is that you typed just after belongs_to, has_many etc…). A correlation is that if you don’t have all your associations set up, you’re dead in the water. If you’ve got one side of a relationship Rails won’t infer the other for you. – If you want to load multiple associations from a model, use an array. – If you want to load an association and some of its child associations then use a hash. The key should be the parent association name, the value should be a description of the child associations.

Now just combine those 3 rules and apply recursively. As an aid here’s a quick snippet that takes an include option (such as [:comments, {:posts => :authors}]) and describes which associations are loaded. The structure is the same as the code in activerecord that handles the :include option, so it should give some insight into how things work.

1
2
3
4
5
6
7
8
9
10
11
12
def describe associations, from = 'base'
  case associations
  when Symbol then puts "load #{associations} from #{from}"
  when Array then associations.each {|a| describe a, from}
  when Hash then
    associations.each do |parent, child|
      raise "Invalid hash - key must be an association name" unless parent.is_a?(Symbol)
      describe parent, from
      describe child, parent
    end
  end
end

This code isn’t too hard to understand. It’s all about the class of the associations parameter – The easy case is if it’s a string or a symbol: what we’ve got is just the name of an association, so just go ahead and load it. – If what we’ve got is an array, then just call ourselves recursively on the contents of that array. – if what we’ve got is a hash, then for each key value pair: – Load the association specified by the key (the parent association) – Load the associations specified by the value (from the from the parent association).

It produces (ugly) output like this:

1
2
3
4
  describe [{:comments => :user}, :category]
load comments from base
load user from comments
load category from base

which is exactly what activerecord would do. If you see something like “load user from comments” but instances of Comment don’t have an association named user then you’ve screwed up.

Examples

Still confused ? Here are some examples, from simple to complicated (these are purely examples of the :include syntax – don’t see this as a recommendation to actually load 10 layers of nested associations). The models are from a hypothetical book selling application and are: – Book – Author – Comment – User

Books belong to authors, and users of the site can leave a comment on any book. The obvious associations are defined. In addition user has a favourite_books association and through the friends association they can list other users who taste in books they generally share.

1
Book.find :all, :include => :author

I hope I don’t have to explain that one to anyone :–)

1
Book.find :all, :include => [:author, :comments]

We want to include both the author and comments, so we place the two names in an array

1
2
3
4
  Book.find :all, :include => [:author, {:comments => :user}]
  Book.find :all, :include => [{:comments => :user}]
  Book.find :all, :include => {:comments => :user}
  Book.find :all, :include => {:comments => [:user]}

In the first example we still want to include author and comments, but now we want to include an association from comments. From the 3rd rule we need a hash containing the key :comments and with corresponding value a description of the associations from the Comment model that we want to load (ie just :user in this case).

In the next 3 examples we just want to include the comments and the user from each comment. These three forms are entirely equivalent (which should be fairly obvious).

1
2
  Book.find :all, :include => {:comments => {:user => :favourite_books}}
  Book.find :all, :include => {:comments => {:user => {:favourite_books => :author}}}

Here we are loading the books’ comments, the user for each comment and the favourite books for each of those users. In the second example we’re also loading the author for each of those favourite books. You can keep on nesting these as far as you want.

1
  Book.find :all, :include => {:comments => {:user => {:favourite_books => [:author, :comments]}}}

Now we’ve come full circle – on each favourite book we’ve loaded the comments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  Book.find :all, :include => {:comments =>
    {:user => [
       :friends,
       {:favourite_books => [:author, :comments]}
]}}
  Book.find :all, :include => {:comments =>
    {:user => [
      {:friends => :favourite_books},
      {:favourite_books => [:author, :comments]}
]}}
  Book.find :all, :include => {:comments =>
    {:user =>
      {:friends => :favourite_books,
       :favourite_books => [:author, :comments]
}}}

Our final examples. In addition to the favourite_books association, we’re loading a user’s friends, and in the second case the favourite books of those friends. The last two examples are identical: we can either have an array with two 1 item hashes, or just one hash with 2 items. We can’t do that in the first example: because we’re not loading any associations from friends we can’t make it into a hash (what would the corresponding value be?)