🦊
【RubyonRails】form_withを使って検索機能を実装する方法
はじめに
RubyonRailsで検索機能を実装するまでのチュートリアルを共有します。
具体的には、以下のような内容で進めます。
- テーブルの設定
- Viewの設定
- Controllerの設定
- Modelの設定
0. 完成イメージ
1. テーブル設定
今回はfoodsテーブルとfood_countriesテーブル、food_categoriesテーブルを作成します。
foodsテーブルにfood_country_idとfood_category_idを外部キーとして設定します。
Foodモデル作成
Foodモデルをマイグレーションし、以下のカラムを追加します。
カラム名 | データタイプ |
---|---|
name | string |
spice_level | integer |
price | decimal{10,2} |
title | string |
body | string |
docker-compose exec web bin/rails g migration CreateFoods name:string spice_level:integer price:decimal{10,2} title:string body:string
# マイグレーション
docker-compose exec web bin/rails db:migrate
FoodCategoryモデル作成
カラム名 | データタイプ |
---|---|
name | string |
docker-compose exec web bin/rails g migration CreateFoodCategories name:string
docker-compose exec web bin/rails g migration AddReferenceFoodCategoriesIdToFoods
マイグレーションファイルの内容
- add_reference :foods, :food_category
- foodsテーブルにfood_categoryという参照カラムを追加
- これはfood_categoriesテーブルのidを参照する外部キー
- foreign_key: { on_update: :restrict, on_delete: :restrict }
- 外部キー制約を追加
- on_update: :restrict:
- food_categoriesのidが更新されると、関連するfoodsのレコードも更新されることを制約
- on_delete: :restrict
- food_categoriesのidが削除されると、関連するfoodsのレコードも削除されることを制限
class AddReferenceFoodCategoriesIdToFoods < ActiveRecord::Migration[7.0]
def change
add_reference :foods, :food_category, foreign_key: { on_update: :restrict, on_delete: :restrict }, comment: '食品カテゴリ'
end
end
マイグレーション
docker-compose exec web bin/rails db:migrate
Railsのアソシエーションを定義
- 関連付け:FoodモデルがFoodCategoryモデルに属していることを示す
- 外部キー:foodsテーブルにfood_category_idカラムが存在し、food_categoriesテーブルのidを参照
- 一対多の関係:一つのFoodCategoryは複数のFoodを持つことができ、各Foodは一つのFoodCategoryに属する
class Food < ApplicationRecord
belongs_to :food_category
end
FoodCountryモデル作成
カラム名 | データタイプ |
---|---|
name | string |
docker-compose exec web bin/rails g migration CreateFoodCountries name:string
docker-compose exec web bin/rails g migration AddReferenceFoodCountriesIdToFoods
マイグレーションファイルの内容
- add_reference :foods, :food_country
- foodsテーブルにfood_countryという参照カラムを追加します。
- これはfood_countriesテーブルのidを参照する外部キーです。
- foreign_key: { on_update: :restrict, on_delete: :restrict }
- 外部キー制約を追加
- on_update: :restrict:
- food_countriesのidが更新されると、関連するfoodsのレコードも更新されることを制限します。
- on_delete: :restrict
- food_countriesのidが削除されると、関連するfoodsのレコードも削除されることを制限します。
class AddReferenceFoodCountriesIdToFoods < ActiveRecord::Migration[7.0]
def change
add_reference :foods, :food_country, foreign_key: { on_update: :restrict, on_delete: :restrict }, comment: '国名'
end
end
マイグレーション
docker-compose exec web bin/rails db:migrate
Railsのアソシエーションを定義
- 関連付け:FoodモデルがFoodCountryモデルに属していることを示します。
- 外部キー:foodsテーブルにfood_country_idカラムが存在し、food_countriesテーブルのidを参照します。
- 一対多の関係:一つのFoodCountryは複数のFoodを持つことができ、各Foodは一つのFoodCountryに属します。
class Food < ApplicationRecord
belongs_to :food_category
belongs_to :food_country
end
2. Viewの設定
form_withの設定
form_withはRailsでフォームを作成するためのヘルパーメソッドです。
- scope: :search
- フォームのパラメータにsearchをプレフィックスとして追加します。これにより、送信されたパラメータがparams[:search]でアクセスできるようになります。
- url: foods_path
- フォームの送信先URLを指定します。
- method: :get
- フォームのHTTPメソッドを指定します。
- local: true
- 非同期でなく、通常のフォーム送信を行う設定です。
<%= form_with(scope: :search, url: foods_path, method: :get, local: true) do |f| %>
<% end %>
各フィールドの設定
- collection_select(プルダウン選択)
- f.collection_select
- フォームビルダーを使ってセレクトボックスを生成します。
- :food_country_id
- フォームのパラメータ名です。選択された値はこのキーで送信されます。
- FoodCountry.all
- セレクトボックスの選択肢として表示するデータの集合です。ここではFoodCountryモデルの全てのレコードを取得します。
- :id
- セレクトボックスの値として使用するカラムです。選択された時に送信される値になります。
- :name
- セレクトボックスに表示されるラベルとして使用するカラムです。
- { selected: @search_params[:food_country_id], include_blank: true }
- selected: @search_params[:food_country_id]
- デフォルトで選択される項目を指定します。
- include_blank: true
- 最初の選択肢を空白にします。
- selected: @search_params[:food_country_id]
- f.collection_select
<%= f.collection_select(:food_country_id, FoodCountry.all, :id, :name, { selected: @search_params[:food_country_id], include_blank: true }) %>
- number_field(価格の範囲指定)
- f.number_field
- 数値入力用のテキストフィールドを生成します。
- :price_from
- フォームのパラメータ名です。入力された値はこのキーで送信されます。
- value: @search_params[:price_from]
- フィールドの初期値を設定します。
- f.number_field
<%= f.number_field :price_from, value: @search_params[:price_from] %> ~ <%= f.number_field :price_to, value: @search_params[:price_to] %>
- text_field(テキストフィールド)
- f.text_field
- フォームビルダーを使用してテキストを入力します。
- :keyword
- フォームのパラメータ名です。入力された値はこのキーで送信されます。
- value: @search_params[:keyword]
- フィールドの初期値を設定します。
- f.text_field
<%= f.text_field :keyword, value: @search_params[:keyword] %>
app/views/foods/search.html.erb
<!-- 検索フォーム -->
<h1> 検索フォーム</h1>
<!-- 条件検索機能-->
<%= form_with(scope: :search, url: foods_path, method: :get, local: true) do |f| %>
<!-- 国から探す(セレクトボックス) -->
<div>
国から探す
<%= f.collection_select(:food_country_id, FoodCountry.all, :id, :name, { selected: @search_params[:food_country_id], include_blank: true }) %>
</div>
<!-- カテゴリから探す(セレクトボックス) -->
<div>
カテゴリから探す
<%= f.collection_select(:food_category_id, FoodCategory.all, :id, :name, { selected: @search_params[:food_category_id], include_blank: true }) %>
</div>
<!-- 値段から探す(範囲指定) -->
<div>
値段から探す
<%= f.number_field :price_from, value: @search_params[:price_from] %> ~ <%= f.number_field :price_to, value: @search_params[:price_to] %>
</div>
<!-- キーワードから探す(テキストフィールド) -->
<div>
キーワードから探す
<%= f.text_field :keyword, value: @search_params[:keyword] %>
</div>
<!-- 検索する -->
<div>
<%= f.submit "検索する" %>
</div>
<% end %>
3. Controllerの設定
Controllerで以下の手順を実行します。
- 受け取った検索パラメータをチェックする(Strong Parameters)
- チェックした検索パラメータを保持する(インスタンス変数)
- 検索パラメータを元に検索する(Scope, includes)
- 検索結果を返す(インスタンス変数)
1. 受け取った検索パラメータをチェックする(Strong Parameters)
検索パラメータは、params[:search]のなかにKey, Valueのセットで検索パラメータが渡されるHash形式で受け取ります。
- food_search_paramsメソッド
- フォームから送信された検索パラメータを安全に受け取ります。
- fetch
- :searchキーのパラメータを取得し、存在しない場合は空のハッシュを返します。
- petmit
- 許可されたパラメータのみを受け取ります。
- :food_country_id:国のIDを指定します。
- :food_category_id:食品カテゴリのIDを指定します。
- :proce_from:検索する価格の下限を指定します。
- :price_to:検索する価格の上限を指定します。
- :keyword:検索キーワードを指定します。
def food_search_params
params.fetch(:search, {}).permit(:food_country_id,
:food_category_id,
:price_from,
:price_to,
:keyword)
end
2. チェックした検索パラメータを保持する(インスタンス変数)
- @search_paramsインスタンス変数
- 検索条件を保持し、Viewで利用可能にします。
3. 検索パラメータを元に検索する(Scope, includes)
- Food.search(@search_params)メソッド
- 検索条件に基づいてFoodデータを取得します。
- searchスコープを使用して条件に合致するFoodデータをフィルタリングします。
- includes(:food_category, :food_country)
- 関連するfood_categoryとfood_countryのデータを事前にロードし、N+1問題を防ぎます。
4. 検索結果を返す(インスタン変数)
- @foodsインスタンス変数
- 検索結果を保持し、Viewで表示します。
# 食品検索結果一覧ページ
def index
@search_params = food_search_params
@foods = Food.search(@search_params).includes(:food_category, :food_country)
end
# トップページ
def search
# 初期化処理
@search_params = {}
end
4. Modelの設定
外部キー制約
- belongs_to :food_category
- FoodモデルがFoodCategoryモデルに属していることを示します。
検索メソッド
- scope :search
- 検索条件を受け取り、結果をフィルタリングします。
- return if search_params.blank?
- 検索パラメータが空の場合は何もせずに終了します。
スコープ
- food_country_id_is:
- food_country_idが指定されている場合、そのIDでフィルタリングします。
- food_category_id_is:
- food_category_idが指定されている場合、そのIDでフィルタリングします。
- price_from:
- priceが指定されたfrom以上のものをフィルタリングします。
- price_to:
- priceが指定されたto以下かつ0より大きいものをフィルタリングします。
- price_toが指定されていない場合は、priceが0以上のものを取得します。
- keyword_like:
- name, title, bodyのいずれかにキーワードが含まれるものをフィルタリングします。
class Food < ApplicationRecord
# 外部キー制約
belongs_to :food_category
belongs_to :food_country
# 検索メソッド
scope :search, -> (search_params) do
# search_paramsが空の場合以降の処理を行わない。
return if search_params.blank?
# パラメータを指定して検索する
food_country_id_is(search_params[:food_country_id])
.food_category_id_is(search_params[:food_category_id])
.price_from(search_params[:price_from])
.price_to(search_params[:price_to])
.keyword_like(search_params[:keyword])
end
# スコープ定義
scope :food_country_id_is, -> (food_country_id) { where(food_country_id: food_country_id) if food_country_id.present? }
scope :food_category_id_is, -> (food_category_id) { where(food_category_id: food_category_id) if food_category_id.present? }
scope :price_from, -> (from) { where('? <= price', from) if from.present? }
scope :price_to, -> (to) {
if to.present?
where('price <= ? AND price > 0', to) # priceが0より大きく、指定したto以下
else
where('price >= 0') # price_to が指定されていない場合の条件
end
}
scope :keyword_like, -> (keyword) {
where('name LIKE :keyword OR
title LIKE :keyword OR
body LIKE :keyword', keyword: "%#{keyword}%") if keyword.present?
}
end
最後に
以上が、RubyonRailsで検索機能を実装するまでのチュートリアルです。
Discussion