Open9

RailsのN+1回避

ダン@HyperFormダン@HyperForm

joins

associationをキャッシュしないのでeager loading(N+1回避)には使えない。

ActiveRecordのオブジェクトをキャッシュしない分メモリの消費を抑えられる。

JOINして条件を絞り込みたいけど、JOINするテーブルのデータを使わない場合はjoinsを使うと良い。

User.joins(:posts).where(posts: { id: 123 })
# SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `posts`.`id` = 123
ダン@HyperFormダン@HyperForm

preload

関連テーブルのデータ(association)を、複数のクエリに分けて取得し、キャッシュする。

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

assosiationの値で絞り込もうとすると、エラーが発生する。

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をeager loadingするときとか、あまりJOINしたくないでかいテーブルを扱うときはpreloadを使うのがよさそう。

ダン@HyperFormダン@HyperForm

eager_load

関連テーブルのデータ(association)を、LEFT OUTER JOINで取得して、キャッシュする。

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`

JOINしているので、preloadと違って、associationの値で絞り込みができる。

User.eager_load(:posts).where(posts: { id: 123 })
# 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` = 123
ダン@HyperFormダン@HyperForm

includes

関連テーブルでの絞り込みが指定されない場合はpreload、指定された場合はeager_loadと同じ挙動になる。

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: 123 })
# 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` = 123
  • includesしたテーブルでwhereによる絞り込みを行っている
  • includesしたassociationに対してjoinsかreferencesも呼んでいる
  • 任意のassociationに対してeager_loadも呼んでいる

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

ダン@HyperFormダン@HyperForm

どう使い分けるか

  • includesは使わない
    • 明示的に書いた方が、処理がブラックボックスにならないし、予期せぬ挙動になることが減る
    • makeって言うより、generateって言う方が意味が限定されて理解しやすくなるのと同じ
  • preloadeager_loadを、関連テーブルでの絞り込みが必要かどうかに応じて使い分ける
  • N+1回避とかじゃなく、単純に絞り込みのためにjoinしたい場合はjoinを使う