Rails|投稿に複数タグを設置する
開発環境
ruby 3.1.2p20
Rails 6.1.7.4
Cloud9
要件
・学校情報(schools)に 学生の国籍タグ(nationality_tags) をつける
・タグは text_field で入力する
・1つの学校情報には 複数の学生の国籍タグをつけられる
・1つの学生の国籍タグは 複数の学校情報につけられる
以上の要件より、以下のようなアソシエーションを作成しました。
※学校情報と国籍タグは多対多の関係になるので、中間テーブル(tagging)も作成します。
モデル作成・アソシエーション
以下のコマンドを実行し、モデルを作成する。
$ rails g model School name:string address:string anual_fee:integer
$ rails g model Tagging school:references tag:references
$ rails g model NationalityTag nationality:string
各モデルにアソシエーションを記述する。
has_many :taggings, dependent: :destroy
has_many :nationality_tags, through: :taggings
belongs_to :school
belongs_to :nationality_tag
has_many :taggings, dependent: :destroy
has_many :schools, through: :taggings
最後に以下のコマンドを実行し、モデルの作成とアソシエーションの設定は完了。
$ rails db:migrate
ルーティング
今回は 学校情報(school)をcreateする際、同時に 国籍タグ(nationality_tag)も作成できるようにします。そのため、schoolモデルのcreateアクションのみ作成します。nationality_tagのcreteアクションは不要です。
resources :schools, only: [:new, :create, :edit, :update]
コントローラ
同様に、コントローラもschoolモデルの分だけ作成します。
$ rails g controller school new edit
def new
end
def create
end
def edit
end
def update
end
ビュー(送信フォーム)
次に、新規登録・編集時のビューを作成します。
タグの入力は、学校情報の登録と同じフォームで入力してもらいます。
また、タグを複数入力する場合は間に半角スペースを入れてもらいます。
<%= form_with model: @school, url: admin_schools_path, method: :post do |f| %>
<div class="field">
<%= f.label :学校名 %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :住所 %><br />
<%= f.text_field :address %>
</div>
<div class="field">
<%= f.label :学費 %><br />
<%= f.text_field :anual_fee %>
</div>
<div class="field">
<%= f.label :学生の国籍 %>(タグは半角スペースで区切ってください)<br />
<%= f.text_field :nationality %>
</div>
<div class="actions">
<%= f.submit "新規登録", class: "btn btn-success" %>
</div>
<%= form_with model: @school, url: admin_school_path(@school), method: :patch do |f| %>
<div class="field">
<%= f.label :学校名 %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :住所 %><br />
<%= f.text_field :address %>
</div>
<div class="field">
<%= f.label :学費 %><br />
<%= f.text_field :anual_fee %>
</div>
<div class="field">
<%= f.label :学生の国籍 %>(タグは半角スペースで区切ってください)<br />
<%= f.text_field :nationality, value: @school.nationality_tags.map(&:nationality).join(" ") %>
</div>
<div class="actions">
<%= f.submit "更新", class: "btn btn-success" %>
</div>
<%= f.text_field :nationality, value: @school.nationality_tags.map(&:nationality).join(" ") %>
この部分について、1つずつ解説します。
f.text_field
テキストの入力フォームを表示させる。
:nationality
この入力フォームの名前。この名前は後でparamsハッシュからデータを取得する際に使用できる。
value:
この属性で入力フォームの初期値を設定できる。
@school.nationality_tags.map(&:nationality).join(" ")
学校情報(@school)に紐づけられた全てのnationality_tagsから、natonalityの値を配列として取得。
→ この部分の記述のおかげで、学校情報の編集画面で、入力済みの国籍タグが表示される。
コントローラにアクションを追記
【create】
def new
@school = School.new
end
def create
school = School.new(school_params)
input_tags = tag_params[:nationality].split(' ')
school.create_tags(input_tags)
school.save
redirect_to schools_path
end
private
def school_params
params.require(:school).permit(:name, :address, :anual_fee)
end
def tag_params
params.require(:school).permit(:nationality)
end
input_tags = tag_params[:nationality].split(' ')
tag_params[:nationality]
で、nationalityタグの文字列を取得し、splitの引数で指定された内容(半角スペース)で区切り、それをinput_tags
に代入している。
school.create_tags(input_tags)
schoolモデルの記述したcreate_tags
メソッドを呼び出し、国籍タグを作成する。
def create_tags(input_tags)
input_tags.each do |tag|
new_tag = NationalityTag.find_or_create_by(nationality: tag)
nationality_tags << new_tag
end
end
create_tags(input_tags)
与えられたinput_tags
を用いて、NationalityTagを作成/既存のものを探すメソッド。
new_tag = NationalityTag.find_or_create_by(nationality: tag)
tag
という名前のNationalityTagが存在すればそれを取得、ない場合は新しく作成し、new_tagへ代入。
nationality_tags << new_tag
nationality_tags
は Schoolモデルと NationalityTagモデルの関連付けを表している。前の行で作成/見つけたnew_tag
を、nationality_tag
に追加する。
※has_many :throug
アソシエーションで <<
が用いられる時、中間テーブルの新しいデータを作成することができる。
わかりやすいように、具体的に説明します。
1️⃣ 入力フォームで ["アメリカ 日本"]
と入力する。
2️⃣ input_tags = ["アメリカ", "日本"]
になる。
3️⃣ new_tag = "アメリカ", new_tag = "日本"
と代入される。
4️⃣ nationality_tags << new_tag
この部分で、schoolモデルに紐づけられる。taggingが2つ作成されることになる。
【update】
次に、updateアクションの記述を確認します。
def edit
@school = School.find(params[:id])
end
def update
school = School.find(params[:id])
school.update(school_params)
input_tags = tag_params[:nationality].split(' ')
school.update_tags(input_tags)
redirect_to admin_school_path(school.id)
end
update_tags
schoolモデルで定義されたメソッド。
def update_tags(input_tags)
registered_tags = nationality_tags.pluck(:nationality)
new_tags = input_tags - registered_tags
destroy_tags = registered_tags - input_tags
new_tags.each do |tag|
new_tag = NationalityTag.find_or_create_by(nationality: tag)
nationality_tags << new_tag
end
destroy_tags.each do |tag|
tag_id = NationalityTag.find_by(nationality: tag)
destroy_tagging = NationalityTagging.find_by(nationality_tag_id: tag_id, school_id: id)
destroy_tagging.destroy
end
end
registered_tags = nationality_tags.pluck(:nationality)
すでに登録されている国籍タグの一覧を取得し、registered_tags
に代入。pluck
は引数に指定したカラムの値を取得するメソッド。
new_tags = input_tags - registered_tags
入力されたタグから、登録済みのタグを引いて、新しく追加すべきタグを計算し、new_tags
に代入する。
destroy_tags = registered_tags - input_tags
登録済みのタグから、入力されたタグを引いて、削除すべきタグを計算し、destroy_tags
に代入する。
new_tags.each do |tag| ... end
新しいタグをデータベースに追加する。流れはcreateアクションと同じ。
destroy_tags.each do |tag| ... end
削除すべきタグをデータベースから削除する。
tag_id = NationalityTag.find_by(nationality: tag)
特定のtag(destroy_tagsの中の1つ)に一致する NationalityTagオブジェクトをデータベースから検索している。このオブジェクトIDをtag_id
に代入。
destroy_tagging = NationalityTagging.find_by(nationality_tag_id: tag_id, school_id: id)
NtionalityTaggingテーブルから、nationality_tag_idがtag_id
で、かつ、school_idがid
のレコードを検索。(この最後のid
はschools_controllerのupdateアクションで指定したschoolのidが取得される。)
destroy_tagging.destroy
destroy_taggingを削除する。
参考にさせていただいた記事
Discussion