はじめに
N+1 問題を解消する手段としてよく挙げられるincludes
メソッド。便利ですよね。このincludes
メソッド、使い方によって挙動が変わると聞いたので、include
メソッドの挙動について調べてみました。
もし間違いなどありましたらご指摘いただけると幸いです。
結論
- 絞り込み条件に、レシーバに対応するテーブルのカラムのみを使用した場合、preloading 方式をとる。(ただし、
references
メソッドと合わせて使うと eager loading 方式をとる) - 絞り込み条件に、レシーバに対応するテーブルとは別のテーブルのカラムを使用した場合、eager loading 方式をとる。
preloading 方式とは
指定したアソシエーションを複数クエリで取得しキャッシュします。
authors = Author.preload(:books).where(age: 20)
/* 発行されるクエリ (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
となります。
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つのクエリで取得しキャッシュします。
authors = Author.eager_load(:books).where(age: 20)
/* 発行されるクエリ */
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を使うので、絞り込み条件には指定したアソシエーションに対応するテーブルのカラムを使用することができます。
authors = Author.eager_load(:books).where('books.genre_id = 1')
/* 発行されるクエリ (最後の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 方式をとります。
authors = Author.includes(:books).where(age: 20)
# は
authors = Author.preload(:books).where(age: 20)
# と同じ挙動
絞り込み条件に、指定したアソシエーションに対応するテーブルのカラムを指定した場合は eager loading 方式をとります。
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
メソッドを使います。
authors = Author.includes(:books).where('books.genre_id = 1').references(:books)
なお、絞り込み条件としてレシーバに対応するテーブルのカラムのみを使用した場合でも、references
メソッドをつけると eager loading 方式となります。
authors = Author.includes(:books).where(age: 20).references(:books)
# は
authors = Author.eager_load(:books).where(age: 20)
# と同じ挙動
preloading 方式をとるか eager loading 方式をとるかの正確なロジックについては参考文献に詳しく書かれているのでそちらをご参照ください。