[Rails] そのincludesはpreloading？それともeager loading？

## はじめに

N+1 問題を解消する手段としてよく挙げられる`includes`メソッド。便利ですよね。この`includes`メソッド、使い方によって挙動が変わると聞いたので、`include`メソッドの挙動について調べてみました。
もし間違いなどありましたらご指摘いただけると幸いです。

## 結論

- 絞り込み条件に、レシーバに対応するテーブルのカラムのみを使用した場合、preloading 方式をとる。（ただし、`references`メソッドと合わせて使うと eager loading 方式をとる）
- 絞り込み条件に、レシーバに対応するテーブルとは別のテーブルのカラムを使用した場合、eager loading 方式をとる。

## preloading 方式とは

指定したアソシエーションを**複数クエリ**で取得しキャッシュします。

```ruby
authors = Author.preload(:books).where(age: 20)
```

```sql
/* 発行されるクエリ　（authorsテーブルにおける authors.age=20 のレコードの id が 1111, 2222, 3333 の場合） */
SELECT `authors`.* FROM `authors` WHERE `authors`.`age` = 20
SELECT `books`.* FROM `books` WHERE `books`.`author_id` IN (1111, 2222, 3333)
```

preloading 方式の場合、絞り込み条件（where句などに指定する条件）にはレシーバに対応するテーブルのカラムのみ使用できます。上記の場合、authorsテーブルのカラムのみ使用できます。
authorsテーブル以外のテーブルのカラムを絞り込み条件に使用すると、エラー`ActiveRecord::StatementInvalid: Mysql2::Error`となります。

```ruby
authors = Author.preload(:books).where('books.genre_id = 1')

# エラーとなる
  ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'books.genre_id' in 'where clause': SELECT `authors`.* FROM `authors` WHERE (books.genre_id = 1)
```

## eager loading 方式とは

指定したアソシエーションを**LEFT OUTER JOINを使って1つのクエリ**で取得しキャッシュします。

```ruby
authors = Author.eager_load(:books).where(age: 20)
```

```sql
/* 発行されるクエリ */
SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2, `authors`.`created_at` AS t0_r3, `authors`.`updated_at` AS t0_r4, `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`author_id` AS t1_r2, `books`.`genre_id` AS t1_r3, `books`.`created_at` AS t1_r4, `books`.`updated_at` AS t1_r5 FROM `authors` LEFT OUTER JOIN `books` ON `books`.`author_id` = `authors`.`id` WHERE `authors`.`age` = 20
```

LEFT OUTER JOINを使うので、絞り込み条件には指定したアソシエーションに対応するテーブルのカラムを使用することができます。

```ruby
authors = Author.eager_load(:books).where('books.genre_id = 1')
```

```sql
/* 発行されるクエリ （最後のWHERE句だけ変わっています） */
SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2, `authors`.`created_at` AS t0_r3, `authors`.`updated_at` AS t0_r4, `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`author_id` AS t1_r2, `books`.`genre_id` AS t1_r3, `books`.`created_at` AS t1_r4, `books`.`updated_at` AS t1_r5 FROM `authors` LEFT OUTER JOIN `books` ON `books`.`author_id` = `authors`.`id` WHERE (books.genre_id = 1)
```

## includes メソッドを使うと？

`preload`メソッドなら preloading 方式、`eager_load`メソッドなら eager loading 方式と一意に決まりますが、`includes`メソッドの場合はこれらをよしなに使い分けます。

絞り込み条件に、レシーバに対応するテーブルのカラムのみ指定した場合は preloading 方式をとります。

```ruby
authors = Author.includes(:books).where(age: 20)
# は
authors = Author.preload(:books).where(age: 20)
# と同じ挙動
```

絞り込み条件に、指定したアソシエーションに対応するテーブルのカラムを指定した場合は eager loading 方式をとります。

```ruby
authors = Author.includes(:books).where(books: { genre_id: 1 })
# は
authors = Author.eager_load(:books).where('books.genre_id = 1')
# と同じ挙動
```

`includes`メソッドを使う場合、絞り込み条件としてアソシエーションに対応するテーブルのカラムを使うには上記の`where(books: { genre_id: 1 })`のように Hash 形式で書く必要があります。
Hash 形式ではなく`eager_load`メソッドを使う場合と同様の書き方をしたければ`references`メソッドを使います。

```ruby
authors = Author.includes(:books).where('books.genre_id = 1').references(:books)
```

なお、絞り込み条件としてレシーバに対応するテーブルのカラムのみを使用した場合でも、`references`メソッドをつけると eager loading 方式となります。

```ruby
authors = Author.includes(:books).where(age: 20).references(:books)
# は
authors = Author.eager_load(:books).where(age: 20)
# と同じ挙動
```

preloading 方式をとるか eager loading 方式をとるかの正確なロジックについては参考文献に詳しく書かれているのでそちらをご参照ください。

## 参考文献

- [Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する（翻訳）](https://techracho.bpsinc.jp/hachi8833/2017_09_25/45650)
- [ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い](https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58)
- [ActiveRecordのincludes, preload, eager_load の個人的な使い分け](https://moneyforward.com/engineers_blog/2019/04/02/activerecord-includes-preload-eagerload/)
- [GitHub上のソース（includesメソッド）](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/query_methods.rb#L96-L138)