🎉

Rails|投稿に複数タグを設置する

2023/09/03に公開

開発環境

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

各モデルにアソシエーションを記述する。

school.rb
  has_many :taggings, dependent: :destroy
  has_many :nationality_tags, through: :taggings
tagging.rb
  belongs_to :school
  belongs_to :nationality_tag
nationality_tag.rb
  has_many :taggings, dependent: :destroy
  has_many :schools, through: :taggings

最後に以下のコマンドを実行し、モデルの作成とアソシエーションの設定は完了。

$ rails db:migrate

ルーティング

今回は 学校情報(school)をcreateする際、同時に 国籍タグ(nationality_tag)も作成できるようにします。そのため、schoolモデルのcreateアクションのみ作成します。nationality_tagのcreteアクションは不要です。

routes.rb
  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 

ビュー(送信フォーム)

次に、新規登録・編集時のビューを作成します。
タグの入力は、学校情報の登録と同じフォームで入力してもらいます。
また、タグを複数入力する場合は間に半角スペースを入れてもらいます。

new.html.erb
  <%= 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>
edit.html.erb
<%= 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】

schools_controller.rb
  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メソッドを呼び出し、国籍タグを作成する。

school.rb

  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アクションの記述を確認します。

schools_controller.rb
  
  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モデルで定義されたメソッド。

school.rb

  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を削除する。

参考にさせていただいた記事

https://qiita.com/ten__/items/1859c72e99400f7d1f9b#最後に
https://zenn.dev/redheadchloe/articles/5eba56d25a7979

Discussion