niki12260714の日記

フリーランスのITエンジニアの呟き。

子モデルで条件を設定し、includesする場合

今でも鮮明に覚えているんですが、初めて先輩に、業務で組んだコードをレビューしてもらった時。
「なんでSelectのループの中で、またSQL発行しているの?」
はい、効率とかリソースとか考えられない超初心者でした。
それ以降、「SQL発行は極力1回で行う、ループの中でループを回さない」というのは意識しています。

そんなこんなで、今作っている画面で問題発生。
質問サイトに投げましたので、どういうことかはこちら参照。

teratail.com

関連するモデルが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問題が発生します。