joinsを使ってeager loadingをする方法です。
その前提の話として
- N + 1 問題
- eager lodingとは
- includesの欠点
- joins
と長くなってしまいましたが、そんなん知ってるという方は最後の
- joinsを使ってeager loadingをする方法
そこだけ見てください。
N + 1 問題
class Guild < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :guild
end
というモデルがあった時
users = User.all
users.each { |user| puts user.guild.name }
みたいなことをした時に(適当ですが)
SELECT `users`.* FROM `users`
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 1 LIMIT 1
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 2 LIMIT 1
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 3 LIMIT 1
users.all
で一回。
users.each { |user| puts user.guild.name }
でusers
の数の分だけSQLが発行されます。
これが N + 1 問題です。この問題を回避するのが eager lodingです
eager loadingとは
users = User.includes(:guild)
上記は以下のSQLを発行されます。
SELECT `users`.* FROM `users`
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` IN (1, 2, 3)
includes
を使用することで2つのSQLを発行し、users
とguilds
の両方を一気に読み込みます。
そのため、users.each { |user| puts user.guild.name }
では一度もSQLを発行することなく処理ができます。
N + 1だったクエリ数が2回になります。この仕組みがeager loadingです。
includesの欠点
eager loadingはパフォーマンス向上のために必要です。
しかし、includes
には問題があります。
User.includes(:guild).where("guilds.id IN (1, 2, 3)")
というようにwhere
句でincludes
で読み込んだテーブルを条件にしようとしたとき
SELECT `users`.`id` AS t0_r0, `users`.`guild_id` AS t0_r1, `users`.`name` AS t0_r2, `guilds`.`id` AS t1_r0, `guilds`.`name` AS t1_r1
FROM `users`
LEFT OUTER JOIN `guilds` ON `guilds`.`id` = `users`.`guild_id`
WHERE (guilds.id IN (1, 2, 3))
というSQLになります。発行されるのはLEFT JOIN
なのでguilds
が存在しない時でもusers
は全部ロードされます。
意図通りであれば問題なのですが、基本的にはこういう場合joins
を使うことが推奨されています。
rails4
rails4ではreferences
を明示的に指定するようになってます。(らしい)
http://qiita.com/joker1007/items/e951d0cf98309dba90db
User.includes(:guild).where("guilds.id IN (1, 2, 3)").references(:guilds)
joins
http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations
関連するテーブルを読み込みwhere
句で関連テーブルを条件にするときはincludes
ではなくjoins
が推奨されています。
User.joins(:guild).where("guilds.id IN (1, 2, 3)")
SELECT `users`.* FROM `users` INNER JOIN `guilds` ON `guilds`.`id` = `users`.`guild_id` WHERE (guilds.id = 1,)
こちらはINNER JOIN
なので先ほどのincludes
の問題がありせん。
しかし、eager loadingが効いていないのです。
users.each { |user| puts user.guild.name }
を実行した時に
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 1 LIMIT 1
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 2 LIMIT 1
SELECT `guilds`.* FROM `guilds` WHERE `guilds`.`id` = 3 LIMIT 1
というSQLが発行されてしまい、N + 1問題が起きます。
joinsを使ってeager loadingをする方法
そこで、joinsを使ってeager loadingをする方法です。
select
を使います。
User.joins(:guild)
.where("guilds.id IN (1, 2, 3)")
.select("users.*, guilds.name AS hoge")
次のSQLが発行されます。
SELECT users.*, guilds.name AS hoge FROM `users`
INNER JOIN `guilds` ON `guilds`.`id` = `users`.`guild_id`
WHERE (guilds.id IN (1, 2, 3))
もちろんですが、SELECT users.*, guilds.name AS hoge
となり、users.*
とguilds.name
がロードされます。
ロードしておきたいguilds
のカラムがたくさんあればその分select
の中に書く必要があります。
ロードしたデータにはuser.guild.hoge
やuser.guild.name
ではなくuser.hoge
という風にアクセスします。
users.each { |user| puts user.hoge }
user.guild.hoge
はもちろんエラーになります。
user.guild.name
ではguild
のロードが実行されてしまってN + 1問題になってしまいます。