44
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-08

はじめに

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 方式をとるかの正確なロジックについては参考文献に詳しく書かれているのでそちらをご参照ください。

参考文献

44
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?