はじめに
先日、アソシエーション関係がある2つのテーブルから、N+1問題を考慮しつつ、特定条件でデータを抽出して表示したいことがありました。
しかし、ActiveRecordの検索メソッド(includes、joins、eager_load、references等)も結構色々あって、
「どれを使えばいいんじゃーー!!」と噴火寸前になりましたので、復習と備忘録を兼ねて整理してみます。
言及しないこと
今回はシンプルに動きだけを確認することを目的としていますので、細かなところは記載しません。
本記事の最後に参考リンクを記載しますので、他の方の記事を参考にしてもらえばと思います。(´ε` )
環境
ruby ruby 2.3.1p112 (2016-04-26 revision 54768)
activerecord (5.0.1)
sqlite3 (1.3.13)
前提条件
モデル(テーブル)のアソシエーション設定及びデータは以下のとおりです。
userさんはitemを持っていて、itemはitemsテーブルに保存されているような一般的な構成です。
class User < ActiveRecord::Base
has_many :items
end
+----+--------+-----+
| id | name | age |
+----+--------+-----+
| 1 | user01 | 10 |
| 2 | user02 | 20 |
| 3 | user03 | 30 |
+----+--------+-----+
class Items < ActiveRecord::Base
belongs_to :user
end
+----+---------+----------------------+-----------+
| id | user_id | content | published |
+----+---------+----------------------+-----------+
| 1 | 1 | user01_true_text | true |
| 2 | 2 | user02_01_true_text | true |
| 3 | 2 | user02_02_true_text | true |
| 4 | 2 | user02_03_false_text | |
+----+---------+----------------------+-----------+
表示したいデータと条件
条件
1)userのid、name、ageを表示したい
2)userが持っているitemを表示したい
3)itemはpublishedがtrueなアイテムのみ表示したい
4)N+1は避けたい
view側のeachは、下記のような感じ。
@users.each do |user|
user.id
user.name
user.age
user.items.each do |item|
item.content
item.published
end
end
やってみる
まずはN+1問題を無視してデータを抽出してぶん回してみる
やりたいこと(3)の要件を満たしていないのでそもそもやる必要もないのですが、一応。
userが持っているitemを取得して表示します。
@users = User.all
User Load (0.1ms) SELECT "users".* FROM "users"
Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."user_id" = ? [["user_id", 1]]
Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."user_id" = ? [["user_id", 2]]
Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."user_id" = ? [["user_id", 3]]
アソシエーション先のデータを表示するところ(user.items.each)で都度SQLが実行されています。
次にincludesを指定してみます。
@users = User.includes(:items)
User Load (0.1ms) SELECT "users".* FROM "users"
Item Load (0.7ms) SELECT "items".* FROM "items" WHERE "items"."user_id" IN (1, 2, 3)
includesを指定すると関係のあるテーブル情報を最小限のクエリ回数で一括読み込してくれます。
一括読み込みすることをeager loadingを言います。
includes + where + references
ここからが本番ですw
itemのpublishedがtrueのレコードを持つuserを抽出して、userとitemの情報を表示します。
@users = User.includes(:items).where(items: {published: true})
SQL (0.2ms) SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."age" AS t0_r2, "items"."id" AS t1_r0, "items"."user_id" AS t1_r1, "items"."content" AS t1_r2, "items"."published" AS t1_r3 FROM "users" LEFT OUTER JOIN "items" ON "items"."user_id" = "users"."id" WHERE "items"."published" = ? [["published", true]]
includes + whereを使うことでアソシエーション先のテーブルカラムに対して、
whereで条件絞ってデータを取り出すことができました。
一回のクエリでアソシエーション先のitemsのデータも取得してくれています。
「LEFT OUTER JOIN」というやつを行っていますね。これは別の記事でまとめていきます。
また表示される内容も問題ありませんでした。
itemsがpublished trueなものしか表示されていません。
・user01のuser01_text
・user02のuser02_02text
注意!
上記にあるようにreferencesというメソッドなしでも動いていますが、
実際には必要なメソッドです。(railsが付けてくれている??)
@users = User.includes(:items).references(:items).published
SQL (0.2ms) SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."age" AS t0_r2, "items"."id" AS t1_r0, "items"."user_id" AS t1_r1, "items"."content" AS t1_r2, "items"."published" AS t1_r3 FROM "users" LEFT OUTER JOIN "items" ON "items"."user_id" = "users"."id" WHERE "items"."published" = ? [["published", true]]
joins + where
次はjoins + whereです。
@users = User.joins(:items).where(items: {published: true})
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "items" ON "items"."user_id" = "users"."id" WHERE "items"."published" = ? [["published", true]]
Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."user_id" = ? [["user_id", 1]]
Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."user_id" = ? [["user_id", 2]]
joins + whereを使うことでアソシエーション先のテーブルカラムに対して、
whereで条件絞ってデータを取り出すことができました。
が、itemsのeachのところで、都度クエリが実行されました。
また、本来表示されてほしくない user02_03_false_textも表示されてしまいました。
itemsのeachのところで発行されているクエリがNGですね。。
eager loadingされていないということですね。
「INNER JOIN」というやつを行っていますね。
joinsはリレーション先の情報を使った検索のみであれば利用可能ですが、
一括読み込みが必要な場合やeager_loadingしたい場合は他のメソッドと組合せる必要があります。
eager_load + where
先読みされていて、表示も問題ありませんでした。
includes + where + referencesと同じクエリを実行しています。
@users = User.eager_load(:items).published
SQL (0.2ms) SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "users"."age" AS t0_r2, "items"."id" AS t1_r0, "items"."user_id" AS t1_r1, "items"."content" AS t1_r2, "items"."published" AS t1_r3 FROM "users" LEFT OUTER JOIN "items" ON "items"."user_id" = "users"."id" WHERE "items"."published" = ? [["published", true]
preload + where
最後にpreload + whereです。
@users = User.preload(:items).where(items: {published: true})
SQLite3::SQLException: no such column: items.published: SELECT "users".* FROM "users" WHERE "items"."published" = ? (ActiveRecord::StatementInvalid)
おっと、、アソシエーション先が検索できずエラーが発生しました。
なお、以下のようにpreloadだけだと、includes(:items)と同じクエリが発行されました。
@users = User.preload(:items)
User Load (0.1ms) SELECT "users".* FROM "users"
Item Load (0.2ms) SELECT "items".* FROM "items" WHERE "items"."user_id" IN (1, 2, 3)
まとめ
今回はとりあえず各メソッドがどんなクエリを発行するのかを中心に確認しました。
環境や実現したいことによって組み合わせはいろいろようです。
eager_loadとincludes+referencesは一見同じように見えますが、
キャッシュの仕様など細かいところでの差異もあるみたい。
また、アソシエーション先のモデルで定義されているscopeを使う場合は、
mergeメソッドを使ってscopeを指定したりしますが、mergeが使える場合は、
eager_loadやjoinsだったりと細かな制約が色々ありました。
以下に記載した「参考にした記事やサイト」には上記の制約や使い分けも詳しく書かれていますので、
ぜひご確認いただければと思います。
参考にした記事やサイト
・Rails における内部結合、外部結合まとめ
様々なパターンで動作を確認しています。ぜひ、参考にしてください。
http://qiita.com/yuyasat/items/c2ad37b5a24a58ee3d30
・ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
xxxxのときはこの組み合わせで!みたいな情報が表で分かりやすくまとまっています!
http://qiita.com/k0kubun/items/80c5a5494f53bb88dc58
・似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ
こちらも分かりやすくていい感じです。
http://qiita.com/south37/items/b2c81932756d2cd84d7d
・ActiveRecordのQueryMethods色々
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html