niki12260714の日記

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

railsで親子モデルのそれぞれに条件設定してn+1問題を解決する方法

親子のモデルで結合する時、joinsかincludesを使うわけですが、取得結果表示で、「子供のループで回したい」となると、joinsだとn+1問題が発生します。
なので、includesをしたいのですが、includesは「親に対してSQL実行」→「親の結果をもとに子のSQLを実行」になるわけです。
とすると、「子が特定の条件を持つ親を取得」はエラーになる。
で、どうするか。
参考にしたのはこちら。

qiita.com

親.joins(子:[孫: [ひ孫: :玄孫]]).where("子.カラムA = ?", 値).preload(:子)

joinsした後にwhere句かけて、preloadするわけです。
※Where句の書き方は、プレイスホルダーの書き方じゃないとエラーが出たので、注意

 

これで親、子それぞれでSQL発行してくれるので、Viewで描画するときに親子でeach文回せます。

→自分課題ですが、これをさらに孫ループもしたい場合はどうすればよいのか……

子モデルで条件を設定し、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問題が発生します。

railsでAjax通信を行い、通信結果を元ページの指定位置にレンダリングする

色々方法はあるみたいですが、自分が理解しやすかった方法。

1.ViewにAjax通信するアクションと、通信結果を受ける個所を記述

f:id:niki12260714:20180531213934j:plain

この例だと、「groupsコントローラーのsearch_memberアクション」がAjax通信行うことになります。
この時に「remote: true」を指定することで、search_memberアクションが終了後に探すViewが「.html.erb」ではなく「js.erb」となって、ページ遷移しません。
そして検索結果を記載するところは、「<div id="search_result"></div>」です。
ここがどうなるかは、後程。

2.Controller側の記載

f:id:niki12260714:20180531214654j:plain

formから飛んできたparamsを元に検索を実施している個所です。

3.通信結果を受け取る「.js.erb」ファイルを作成する
この例ですと、Viewに「search.js.erb」というファイルを作成することになります。
拡張子から分かるように、JavaScriptのファイルになります。

f:id:niki12260714:20180531215228j:plain

記述は1行。
最初の「$('#search_result')」が、1で記述した「div id="search_result"」に対して動作をすると指定します。
続く「render~」以下は、「search_resultという名前のパーシャルをレンダリングしますよ、@invitation_userを受け渡します」という記述になります。

4.レンダリングされるパーシャルで検索結果を記述
3で「search_result」というパーシャルを指定しているので、Viewに「_search_result.html.erb」というファイルを作ります。
ここに検索結果をどう表示するのかを記載。

f:id:niki12260714:20180531215737j:plain

3で「@invitation_user」を受け渡されていますので、Viewで使えます。

なんか沢山ファイルが出来ますが、自分が理解しやすかったのはこの方法でした。
3のところでJavaScriptでもって直接書き換えるのもできると思うんですが、自分はJavaScriptの記述に慣れていないので、パーシャル使ってViewの方であれこれする方が分かりやすかったです。
いずれはもうちょっとすっきり記述したいところ。

親子孫の3つのテーブルを関連付けてデータ取得

 A,B,Cというテーブルがあって、それぞれ親→子→孫という関係とします。

Aモデル:
has_many:bs(複数形)

Bモデル:
belongs_to:a(単数形)
has_many:cs(複数形)

Cモデル:
belongs_to:b(単数形)

ここで、cモデルにあるカラムをwhere句にして、Aモデルの値を取るとします。
やり方としては、

・Cモデルにscopeを定義する
scope :c_scope, ->(id) {where("c_id = ?", id)}

・Bモデルで、Cモデルをマージして、Cのスコープを呼び出すスコープを定義する
scope :b_scope, ->(id){
   joins(:cs).merge(C.c_scope(id))
}

・Aモデルで、Bモデルをマージして、Bのスコープを呼び出すスコープを定義する
scope :a_scope, ->(id){
   joins(:bs).merge(B.b_scope(id))
}

で、Aモデルのスコープ(a_scope)を呼び出すことで、Cスコープまで値を渡す。

……本当に合っているか不明。もうちょっとスマートに書けないものかな?

親子関係にあるテーブルに一度にデータを保存する

親テーブルにデータを作ったら、そのidを使って子テーブルにもデータを入れたい場合がありますよね。
というのをやってくれるのが、buildです。

まず最初に、各モデルに親子関係を記述します。
※Aの子供がB、Bの子供がCとする

Aモデル:
has_many:bs(複数形)

Bモデル:
belongs_to:a(単数形)
has_many:cs(複数形

Cモデル:
belongs_to:b(単数形)

※1:1の場合はhas_one:単数形になることに注意。そしてこの場合、buildではなく、build_xxになる。
参考:http://codenote.net/ruby/rails/1778.html

で、親のインスタンスを作成したら、子供のインスタンスをbuildする。

oya = A.new
child = oya.bs.build
childchild = child.cs.build

※親から見た子供は複数形で記述

※親から孫の生成はできない(例:oya.bs.cs.buildみたいにはできない)

※子供のインスタンスに固定値を入れておきたい場合は引数に記述する
参考:build - リファレンス - - Railsドキュメント

実際にDBに保存する場合は、親だけを保存すれば、一緒に子供のテーブルにも保存してくれるし、親のIDも自動的に取得して入れてくれる。

oya.save

AWS Cloud9でPostgreSQLに対しrake db:migrateが失敗する

ここら辺、自動でやってくれるDBとかで開発していたんで、知らないから躓いたところでした。
前回の記事の続きです。

niki12260714.hatenablog.com

ここの記事で、「confファイルが見つからなかった」とありますが、ここが問題でした。

migrateのファイルを作り、「rake db:migrate」を打つと、「FATAL: Ident authentication failed for user」というエラーが出て、rakeが失敗します。
調べてみると、見つからなかった「pg_hba.conf」というのが認証ファイルで、ここのデフォルト値が「OSのユーザーIDとpostgreのユーザーIDが一致していれば受け入れる(ident)」になっているのが問題ということ。
なので、ここを「パスワード認証(md5)」か「無条件で信頼(trust)」に書き換える必要があります。
しかしどうにも見当たらないので、ここは講師の人に聞いたところ

sudo vim /var/lib/pgsql94/data/pg_hba.conf

でいけますよ、ってことで、行けました!
※sudoでやらないと拒否されるフォルダの中にあるから、「vim」だけで打っていった時に見当たらなかったのだと思われる

で、md5に書き換えたのですが、そうすると今度は「sudo -u postgres psql」が効かなくなりました。
正確には、これを打つとパスワードを聞かれるのですが、postgresロールのパスワードを入れても拒否される。
ここは調べてもちょっと分からなかったんですが、Unixのroot権限が通らなくなったのが原因らしい?
仕方ないってことで、先ほどの「pg_hba.conf」を全てtrustに変更するとpsqlを打てるようになりました!
ここでrake db:migrateを実行し、無事に通りました。

※ちなみにconfを書き換えたら「sudo /etc/init.d/postgresql94 restart」でポスグレを再起動する必要があります
※Cloud9は暫く触っていないとポスグレを止めちゃうので、通らなくなったと思ったら「sudo /etc/init.d/postgresql94 start」で開始する必要があります