やりたいこと
1つの検索フォームで、複数の値を同時に検索したい。
今回は、customersテーブルのname_kana、phoneとreservationsテーブルのcheck_inの日付範囲検索を1つの検索フォームで実装する
前提条件
下記のように、一人のcustomerが多くのreservationsを持つ、1対多の関係。
has_many :reservations
belongs_to :customer
実装方法
1.reservationモデルに、scopeを使って複数の検索条件を定義する。
2.reservationsコントローラにsearchアクションを定義。その際に、①で作ったscope(search)を利用する。各フォームのparamsは1つのハッシュにしてまとめて受け取る。
3.ビューを作成する。
※scopeとは・・・複数のクリエ(SQL文による検索条件)をまとめたメソッドを定義できるメソッド
実装
1.reservationモデルに、scopeを使って複数の検索条件を定義する。
class Reservation < ApplicationRecord
belongs_to :customer
scope :search, -> (search_params) do #scopeでsearchメソッドを定義。(search_params)は引数
return if search_params.blank? #検索フォームに値がなければ以下の手順は行わない
name_kana_like(search_params[:name_kana])
.check_in_from(search_params[:check_in_from])
.check_in_to(search_params[:check_in_to])
.phone_like(search_params[:phone]) #下記で定義しているscopeメソッドの呼び出し。「.」で繋げている
end
scope :name_kana_like, -> (name_kana) { where('name_kana LIKE ?', "%#{name_kana}%") if name_kana.present? } #scopeを定義。
scope :check_in_from, -> (from) { where('? <= check_in', from) if from.present? }
scope :check_in_to, -> (to) { where('check_in <= ?', to) if to.present? }
#日付の範囲検索をするため、fromとtoをつけている
scope :phone_like, -> (phone) { where('phone LIKE ?', "%#{phone}%") if phone.present? }
#scope :メソッド名 -> (引数) { SQL文 }
#if 引数.present?をつけることで、検索フォームに値がない場合は実行されない
end
上から、scope :メソッド名, -> (引数) do 〜〜 end で、:searchというメソッド名のscopeを定義している。
今回引数にしているsearch_paramsは、この後reservationコントローラから渡す引数の名前と同じにしています。
name_kana_like(search_params[:name_kana])
.check_in_from(search_params[:check_in_from])
.check_in_to(search_params[:check_in_to])
.phone_like(search_params[:phone])
この部分は一見わかりずらいですが、各scopeのメソッドを繋げているだけです。
where('search_params[:name_kana] LIKE ?', "%#{search_params[:name_kana]}%") if search_params[:name_kana].present? . where('? <= check_in', search_params[:check_in_from]) if search_params[:check_in_from].present? . where('check_in <= ?', search_params[:check_in_to]) if search_params[:check_in_to].present? . where('phone LIKE ?', "%#{search_params[:phone]}%") if search_params[:phone].present?
蓋を開けると上記のようになるので、Model.where('id>?',3)みたいなコードのwhere文が連結していて、検索条件がいっぱいあるようなイメージです。
name_kanaと、phoneはあいまい検索。check_in_to以上、check_in_from以下で検索。
各フォームにデータがなければその検索は行わない。
上記の条件を同時に定義した形です。
2.reservationsコントローラにsearchアクションを定義。その際に、①で作ったscope(search)を利用する。
def search
@search_params = reservation_search_params #検索結果の画面で、フォームに検索した値を表示するために、paramsの値をビューで使えるようにする
@reservations = Reservation.search(@search_params).joins(:customer) #Reservationモデルのsearchを呼び出し、引数としてparamsを渡している。
end
private
def reservation_search_params
params.fetch(:search, {}).permit(:name_kana, :check_in_from, :check_in_to, :phone)
#fetch(:search, {})と記述することで、検索フォームに値がない場合はnilを返し、エラーが起こらなくなる
#ここでの:searchには、フォームから送られてくるparamsの値が入っている
end
下のストロングパラメーターから。
この後、ビューのform_withに、scope: :searchというオプションをつける。
params.fetch(:search, {})の:searchには、検索フォームに入力されたデータの全てが、1つのハッシュとして送られている。
[1] pry> params[:search]
=> {"check_in_from"=>"", "check_in_to"=>"", "name_kana"=>"", "phone"=>"09000000000"}
fetchは、引数にキーを渡すことで、その値を返してくれるメソッド。このfetchの第二引数に、{}を渡すことで、検索フォームの全てに値がない場合でもエラーが起こらなくなります。
permitの後は、いつも通り受け取りたいキーを記述してください。
permit(:name_kana, :check_in_from, :check_in_to, :phone)
check_inカラムは範囲検索をするので、fromとtoを加えて独自の名前をつけています。form内、モデルのscope内で使う名前と統一していればなんでもいいはずです。
searchアクションでは、上記で説明したストロングパラメーターの値を変数化させることで、ビューでparamsの値を使えるようにします。
検索結果画面に、検索をした値がフォームに残るようにさせるためです。
@reservations = Reservation.search(@search_params).joins(:customer)
で、①で作成したreservationモデルのscope(search)を呼び出しています。
その際に、引数にストロングパラメータの値を渡すことで検索させています。
アソシエーションさせているので、includesが使えると思うのですが、なぜかうまく行かないのでjoinsを使ってます。わかる方がいれば教えてください。
上記の検索結果をビューに持っていくために、reservationsという変数に格納しています。
searchアクションのルーティングも作成しておきます。
resources :reservations, only: [:index, :show] do
collection do
get :search
end
end
3.ビューを作成する。
<%= form_with(scope: :search, url: search_reservations_path, method: :get, local: true) do |f| %>
<%= f.label :check_in, "到着日" %>
<%= f.date_field :check_in_from, value: @search_params[:check_in_from] %> ~
<%= f.date_field :check_in_to, value: @search_params[:check_in_to] %>
<%= f.label :name_kana, "ナマエ" %>
<%= f.text_field :name_kana, placeholder: '半角のみ', value: @search_params[:name_kana] %>
<%= f.label :phone, "電話番号" %>
<%= f.text_field :phone, value: @search_params[:phone] %>
<%= submit_tag '検索', class: "button" %>
<% end %>
form_withのオプションでscope: :searchとすることで、各フォームのparamsが、:searchという名前の1つのハッシュにまとめられているイメージです。それをコントローラのストロングパラメータで受け取っていました。
urlは②で作成したsearchアクションのパスを指定。
form_withはデフォルトでremote: trueなので、local: trueを記述。
各formに、value: @search_params[:キー]をつけることで、検索後に検索した値を表示することができます。
<% unless @search_params.blank? %>
<% @reservations.each do |reservation| %>
<tr>
<td><%= reservation.customer.name_kana %></td>
<td><%= reservation.customer.phone %></td>
<td><%= reservation.check_in %></td>
</tr>
<% end %>
<% end %>
reservationsコントローラで取得した検索結果をeachで回すことでヒットした情報を表示!
そのままだと、検索画面に遷移した時に全てのレコードが取得されて表示されていたので、search_paramsに検索したデータがない場合はデータを表示させないようにしています。
備考
参考にさせていただいた記事
【Rails】一覧ページ上部に検索機能を実装する ~ form_with ~
真似して書いたらなんかできた!ってレベルだったので、理解を深めたくて記事にしました。
全く意味がわからない!から、多少読めたになった程度で、いまいち理解しきれてない・・・。
何か間違いなどがあればぜひご指摘ください。