ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い

ActiveRecordでN+1クエリを潰すためにeager loadingを行う場合、`preload`や`includes`や`eager_load`が役に立つ。
[Preload, Eagerload, Includes and Joins](http://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html)という記事にそれらの違いがよくまとめられているんだけど、includesが挙動を変える条件があまり正確に書かれていなくて自信が持てなかったし、そもそも記事が古いのでRails4.1.5のソースを読んで調べた。

せっかく調べたので、全体を通して日本語でまとめてみようと思う。

## joins
```rb
User.joins(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
```
デフォルトでINNER JOINを行う。LEFT OUTER JOINを行いたい時は`left_joins`を使う。
他の3つとの違いは、associationをキャッシュしないこと。
associationをキャッシュしないのでeager loadingには使えないが、ActiveRecordのオブジェクトをキャッシュしない分メモリの消費を抑えられる。

なので、JOINして条件を絞り込みたいが、JOINするテーブルのデータを使わない場合は`joins`を使うのが良い。
`User.joins(*).where(posts: {*})`とか`User.joins(*).merge(Post.*)`みたいに使う。

## eager_load
```rb
User.eager_load(:posts)
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

User.eager_load(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
```

指定したassociationをLEFT OUTER JOINで引いてキャッシュする。
クエリの数が1個で済むので場合によっては`preload`より速い。
JOINしているので、`preload`と違って、`joins`と同じようにJOINしたテーブルで絞込ができる。

## preload
```rb
User.preload(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

User.preload(:posts).where(posts: { id: 1 })
# SELECT `users`.* FROM `users`  WHERE `posts`.`id` = 1
# => Mysql2::Error: Unknown column 'posts.id' in 'where clause': SELECT `users`.* FROM `users`  WHERE `posts`.`id` = 1
```

指定したassociationを複数のクエリに分けて引いてキャッシュする。
複数のassociationをeager loadingするときとか、あまりJOINしたくないでかいテーブルを扱うときはpreloadを使うのがよさそう。
preloadしたテーブルによって絞り込もうとすると、例外を投げる。

## includes
```rb
User.includes(:posts)
# SELECT `users`.* FROM `users`
# SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, ...)

User.includes(:posts).where(posts: { id: 1 })
# SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`created_at` AS t0_r2, `users`.`updated_at` AS t0_r3, `posts`.`id` AS t1_r0, `posts`.`user_id` AS t1_r1, `posts`.`created_at` AS t1_r2, `posts`.`updated_at` AS t1_r3 FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 1
```

- `includes`したテーブルで`where`による絞り込みを行っている
- `includes`したassociationに対して`joins`か`references`も呼んでいる
- 任意のassociationに対して`eager_load`も呼んでいる

のうちいずれかを満たす場合、`eager_load`と同じ挙動(LEFT JOIN)を行い、
そうでなければ`preload`と同じ挙動(クエリを分けて実行)をする。
絞り込みが必要な時に例外を投げず`eager_load`にfallbackする`preload`。

#### 根拠
```rb
def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
    includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end
```

[ActiveRecord::Relation#eager_loading?](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation.rb#L556-L560)が[trueの場合](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation.rb#L611)LEFT JOINを行い、[falseの場合](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation.rb#L614)クエリを分けて読み込む。

`eager_load_values`とか`includes_values`はeager_loadしたやつとincludesしたときに追加されるやつで、そのまま。
[joined_includes_values](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation.rb#L566-L568)はincludesしたやつをわざわざjoinsもしてる奴のリスト。

[references_eager_loaded_tables?](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation.rb#L626-L641)はJOIN元以外に`references`が行われている場合にtrueになる。なお、whereで別テーブルによる絞り込みを行った場合も[referencesが行われる](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation/query_methods.rb#L569)ため、この場合もtrueになる。

ちなみに、[`references`は`includes`のスイッチング専用メソッドである](https://github.com/rails/rails/blob/9c297ce93610bcd878f5a5ca8e737bf057fc2d85/activerecord/lib/active_record/relation/query_methods.rb#L176)。

## まとめ
|メソッド   |キャッシュ|クエリ     | 用途         |
|---------:|:-------:|:--------:|:------------|
|joins     |しない    |単数      |絞り込み          |
|eager_load|する      |単数      |キャッシュと絞り込み|
|preload   |する      |複数      |キャッシュ     |
|includes  |する      |場合による |キャッシュ、必要なら絞り込み|

そのテーブルとのJOINを禁止したいケースでは`preload`を指定し、JOINしても問題なくてとりあえずeager loadingしたい場合は`includes`を使い、必ずJOINしたい場合は`eager_load`を使いましょう。