Open9
RailsのN+1回避
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
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を使うのがよさそう。
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
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。
注意点
includes
は、preload
とeager_load
をよしなに使い分けてくれる。
だからキャッシュしたいassociationが1つの場合、とりあえずincludesを使っておけば問題なさそう。
ただし、キャッシュしたいassociationが複数ある場合、「全てpreload」「全てeager_load」のどちらかになる。
Rails4以前と5以後でincludesの挙動が違う?
どう使い分けるか
-
includes
は使わない- 明示的に書いた方が、処理がブラックボックスにならないし、予期せぬ挙動になることが減る
- makeって言うより、generateって言う方が意味が限定されて理解しやすくなるのと同じ
-
preload
とeager_load
を、関連テーブルでの絞り込みが必要かどうかに応じて使い分ける - N+1回避とかじゃなく、単純に絞り込みのためにjoinしたい場合は
join
を使う