niki12260714の日記

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

railsで二つのセレクトボックスを連動させる

都道府県のセレクトボックス選ぶと、隣にある市町村のセレクトボックスの中身が変わる。
そういう奴をrailsで作る方法です。
ネットに沢山情報がありますが、自分が分かりやすかった方法がこちら。

【プログラムの動き】
1.セレクトボックスにonchangeイベントを設定し、coffeescriptに記述した関数を呼び出す
2.呼び出された関数で、セレクトボックスの選択値を取得、Ajax通信を行い、データを取得
3.取得したデータを、coffeescriptでセレクトボックスの要素に設定する

というわけで、1から順番に。

【View】
<%= select_tag 'parent_select', options_for_select(@date_array), onchange: 'change_data()' %>

赤字のところで、「セレクトボックスが選択されたら、change_dataという関数を呼び出す」と設定します。

coffeescript

f:id:niki12260714:20180620175754j:plain

coffeescriptはインデントが大事なので、画像でコードを表示
app/assets/javascriptの中に配置されるcoffeescriptに記述します。
自分が詰まった場所について、それぞれ解説。
「4行目:$ ->」は、「jQueryを使用します」という宣言。
「5行目:@change_data =() ->」は、先ほどViewで設定した呼び出し関数ですが、ここでポイントは「@」と「()」。
coffeescriptのサイトでコンパイルすると分かりますが、@はthisを表します。
これがいないと、View側から見えないので、セレクトボックスを選択するとエラーになっちゃいます。
()も、呼び出される時には必要な設定。
「7行目:val = $('[name=parent_select]').val()」は、jQueryを使ってセレクトボックスで選択された値を取ってきます。
9~18行目はAjax通信部分で、json形式の戻り値を処理します。
処理は別関数に切り出してます、21行目以下。
jsonをfor文で回し、中身をoptionの中に入れてセレクトボックスの末尾に追加。

 

【controller】
def
 data = Item.where(id: params[:id])
 render json: group.select("id AS id, item_name AS name")
end

 Ajax通信の個所。
render jasonで、json形式にデータを展開します。

以上!

AWS Cloud9でpumaを再起動

参考にしたのはこちら。

qiita.com

ターミナルで「ps ax」を打つと、これが出てきました。

f:id:niki12260714:20180613171258j:plain

AWSの場合、「puma x.xx.xx~」を殺せばよい模様。
「kill -9 6109」でpumaが終了するので、「rails s -b $IP -p $PORT」を打てば再起動する。

※「server.pid」ファイルをrmする必要があるかは不明。多分rmしなくても良い気がする……。

【調査中】railsでleft_joinsするモデルに対して条件を追加する方法

こちらと同じことをしたくて調べています。

teratail.com

自分の場合、まさに外部結合する方のテーブルに条件を追加するSQLを発行したいのです。

SELECT A.id, B.id
FROM A
LEFT OUTER JOIN B ON A.id = B.a_id AND B.col = (動的な値)
WHERE A.col = (なんかの値)

赤字のところを追加したいんですが、これが分からん。
あんまりモデルにデフォルトスコープ持たせたくないのと、後でメンテするときに分かりやすい記述にしておくべきと思うので、今はfind_by_sqlSQLべた書きにしています。
これを折々調べようと思うんで、ここにメモとして残しておきます。

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文回せます。

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

railsで親子孫ひ孫までをincludesする

自分メモ。
沢山ある関連モデルをincludesする場合。

親.where(条件文).all.includes(子: [孫: :ひ孫])

子モデルで条件を設定し、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の方であれこれする方が分かりやすかったです。
いずれはもうちょっとすっきり記述したいところ。