子モデルで条件を設定し、includesする場合
今でも鮮明に覚えているんですが、初めて先輩に、業務で組んだコードをレビューしてもらった時。
「なんでSelectのループの中で、またSQL発行しているの?」
はい、効率とかリソースとか考えられない超初心者でした。
それ以降、「SQL発行は極力1回で行う、ループの中でループを回さない」というのは意識しています。
そんなこんなで、今作っている画面で問題発生。
質問サイトに投げましたので、どういうことかはこちら参照。
関連するモデルが3つあって、親子それぞれにwhere句発行したいというもの。
取得結果をViewの方で、親をeach文で回して親のデータ、さらには子のデータを展開したい。
親を描画するたびにSQL発行されたら困るんで、親子はincludesしなければならないんですね。
ここがややこしくなったところなんですが、私が欲した条件って、「親モデルに対する条件が、子モデルにもある」ということなんです。
だから、includesした子モデルに対して条件を発行することはなかったんですね。
一生懸命、includesの子モデルにwhere句を発行する方法を探していたんですが、そんな方法はないわけで。
【Where句をかけて絞った親モデルに対し、子モデルをincludesする】
これが正しいやり方だったわけです。
実際のやり方。
1.Where句をかけて、親モデルを生成する
自分はこれを、scopeでやっています。
#groupsモデルに、group_membersをjoinして、Where句となるスコープを定義する
scope :get_group_list, ->(uid){
where("cola > ?", Date.today).\
joins(:group_members).\
merge(GroupMember.own_group_member2(uid))
}
#group_membersモデルにも、条件となるスコープを定義する
scope :own_group_member2, -> (uid){
where(user_id: uid)
}
こうやってscopeを定義しておいて、親モデルから呼び出します。
呼び出し方はこちら。
@group_list = Group.get_group_list(current_user.id).all
※ここでallをかけないと、続くincludesでそれぞれのモデルでselectしてくれないので注意
2.絞られた親モデルに対し、子モデルをincludesしていく
@group_list = Group.get_group_list(current_user.id).all.includes(group_members: :user)
赤字の個所が結合の個所です。
これで、「groupsモデル」「group_membersモデル」「usersモデル」に対し、1回ずつSQLを発行します。
発行結果はキャッシュされているので、groupsモデルをeachで回している時に、その子供のgroup_membersを呼び出せば、そのグループに紐づくメンバー情報を取得できます。
SQLは発行されませんので、n+1問題は発生しません。
【注意点】
・1で制限するときにorder_by句が入っていると、includesが上手くいきません。
一行の長いSQLが発行され、n+1問題が発生します。