はじめに
テーブルを結合するための複数あるメソッドの使い分けがあやふやなのでここで調べたことなどをまとめます。
得られる結果が同じだからと言って意識しないでいると思わぬところで足を掬われそう。
joins
結合先のテーブルの情報を使わない時に使う。→メモリ削減
内部結合INNER JOIN
を行う。
- 結合先のテーブルで絞り込むことが可能
- 結合先のテーブルの情報を取得できない
User.joins(:user_profiles)
# 発行されるSQL
SELECT "users".* FROM "users" INNER JOIN "user_profiles" ON "user_profiles"."user_id" = "users"."id"
preload
結合をしたくない時に使用する→テーブルの規模が大きく、動作が極めて重くなる場合(has_many関係)や、LEFT JOIN時のnullを発生させたくない場合など
SQLを二回発行し、擬似的にJOINしてデータを取得する。
- 結合先のテーブルで絞り込むことが不可能→例外が発生する。
- 結合先のテーブルの情報を取得できない
User.preload(:user_profiles)
# 発行されるSQL
SELECT "users".* FROM "users"
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" IN (1,2,..)
eager_load
外部結合をさせたい時に使用する→他のメソッドと違い、必ず外部結合したクエリ1つを発行するため、効率的かつ早い。(has_one、belongs_to関係に有効)
外部結合LEFT OUTER JOIN
を行う。
- 結合先のテーブルで絞り込むことが可能
- 結合先のテーブルの情報を取得できる
User.eager_load(:user_profiles)
# 発行されるSQL
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "user_profiles"."id" AS t1_r0,
"user_profiles"."address" AS t1_r1, "user_profiles"."user_id" AS t1_r2, "user_profiles"."tel" AS t1_r3
FROM "users" LEFT OUTER JOIN "user_profiles" ON "user_profiles"."user_id" = "users"."id"
includes
preloadとeager_loadのどちらかの挙動を行う。→JOINしても問題ない場合に使う。
結合先のテーブルを絞り込もうかするかどうかで挙動を変える。
※関連テーブルが複数になる場合、個別に挙動を指定できない点は注意する。
- 結合先のテーブルで絞り込まない場合はpreloadと同じ挙動
User.includes(:user_profiles)
# 発行されるSQL
SELECT "users".* FROM "users"
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" IN (1,2,..)
- 結合先のテーブルで絞り込む場合はeager_loadと同じ挙動
User.includes(:user_profiles)where( "user_profiles.address = 'Tokyo'").
# 発行されるSQL
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "user_profiles"."id" AS t1_r0,
"user_profiles"."address" AS t1_r1, "user_profiles"."user_id" AS t1_r2, "user_profiles"."tel" AS t1_r3
FROM "users" LEFT OUTER JOIN "user_profiles" ON "user_profiles"."user_id" = "users"."id" WHERE (user_profiles.add = "Tokyo")
incledesの取り扱いについて
preloadとeager_loadを内包しているため、一見良さそう。
しかし、状況に応じて挙動が変わる上にコントロールできないため、思わぬ不具合につながるかも?
includesを使わないべきという資料も複数見つけられた
includesはクエリが状況によって変わってコントロールしずらいので基本使わないようにしています。
引用: ActiveRecordのincludes, preload, eager_load の個人的な使い分け
includesはなるべく利用しない方が良い(理由は後述します)
理由:意図しない挙動を防ぐため
引用: ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由
判断が明確にできる場合はなるべくpreload
かeager_load
を使う方がよいが、後の改修を考えたらどちらの挙動も取れるincludes
を使うメリットもあるといえる。
あとは、preload
(joins
も)は結合先のテーブルのデータを参照したり絞り込みに使えないため、結合先のデータを使用したいかどうかが判断の一つの基準になりそう。
モデルなどにメソッドとして共通化する場合に使用する際は特に気をつけたい
-
preload
かeager_load
挙動を完全に決定できるが、その分メソッド自体の汎用性、テーブルのデータ量の移り変わりに弱い -
incledes
結合先のデータを参照できるほか、テーブルのデータ量にあわせてエンジニア側で挙動が変わるようにするなど、うまく使うことができれば汎用性に優れるといえる。ただし、メソッドの使用先の状況で挙動が変わるというのは管理やコントロールが困難になる可能性が大きい。
参考
https://zenn.dev/tomoya_pama/articles/85a37b3f1e6119
https://zenn.dev/mithuami/articles/c4b0e9694182d1
https://zenn.dev/d0ne1s/articles/fdf4832d478da6
https://zenn.dev/kibe/articles/c043a7807fa7d0
https://moneyforward-dev.jp/entry/2019/04/02/activerecord-includes-preload-eagerload/
https://qiita.com/ryosuketter/items/097556841ec8e1b2940f