💡

Rails|モデルに複数のタグをつける方法

2023/09/10に公開

目標

学校情報を作成時、学生の国籍を半角スペース区切りで複数入力できる。
学校情報詳細ページで、学生の国籍がカンマ区切りで表示される。
学校情報編集ページで、学生の国籍が半角スペース区切りで表示される。

開発環境

ruby 3.1.2p20
Rails 6.1.7.4
Cloud9

前提

Schoolモデルを作成済み
schools_controllerを作成済み

全体の流れ

1日本語学校は、複数の学生の国籍を持つ。
1学生の国籍は、複数の日本語学校を持つ。
日本語学校と学生の国籍は多対多の関係になるので、中間テーブル(国籍タグづけ)を作成する。

ER図

モデルについて

モデルを作成する。(※私はモデル名を無駄に長くしてしまいました...これは失敗です><)

ターミナル
$ rails g model student_nationality_tag nationality:string
$ rails g model student_nationality_tagging schools:references student_nationality_tag:references

DB反映を忘れずに。

ターミナル
$ rails db:migrate

各モデルにアソシエーションを記述する。schoolモデルには、後で使うメソッドも追記する。

schools.rb
  has_many :student_nationality_taggings, dependent: :destroy
  has_many :student_nationality_tags, through: :student_nationality_taggings
  
  def create_tags(input_tags)
     input_tags.each do |tag|
       new_tag = StudentNationalityTag.find_or_create_by(nationality: tag)
       student_nationality_tags << new_tag
     end
  end

  def update_tags(input_tags)
    registered_tags = student_nationality_tags.pluck(:nationality)
    new_tags = input_tags - registered_tags
    destroy_tags = registered_tags - input_tags

    new_tags.each do |tag|
      new_tag = StudentNationalityTag.find_or_create_by(nationality: tag)
      student_nationality_tags << new_tag
    end

    destroy_tags.each do |tag|
      tag_id = StudentNationalityTag.find_by(nationality: tag)
      destroy_tagging = StudentNationalityTagging.find_by(student_nationality_tag_id: tag_id, school_id: id)
      destroy_tagging.destroy
    end
  end
student_nationality_tag.rb
  has_many :student_nationality_taggings, dependent: :destroy
  has_many :schools, through: :student_nationality_taggings
student_nationality_tagging.rb
  belongs_to :school
  belongs_to :student_nationality_tag

ルーティングの記述

新しく作成したモデルは、schoolの新規作成時や更新時に一緒に作成・更新されるので、個別のルーティングは不要。

routes.rb
  resources :schools

コントローラの記述

schools_controller.rb
class Admin::SchoolsController < ApplicationController
  before_action :authenticate_admin!

  def new
    @school = School.new
  end

  def create
    school = School.new(school_params)
    input_tags = tag_params[:nationality].split(' ')
    school.create_tags(input_tags)
    if school.save
      flash[:notice] = "学校情報を登録しました"
      redirect_to admin_schools_path
    else
      flash[:alert] = school.errors.full_messages.join(", ")
      render :new
    end
  end

  def edit
    @school = School.find(params[:id])
  end

  def update
    school = School.find(params[:id])
    if school.update(school_params)
      input_tags = tag_params[:nationality].split(' ')
      school.update_tags(input_tags)
      flash[:notice] = "学校情報を更新しました"
      redirect_to admin_school_path(school.id)
    else
      @school = School.find(params[:id])
      flash[:alert] = school.errors.full_messages.join(", ")
      render :edit
    end
  end
  
  def show
    @school = School.find(params[:id])
  end

  private

    def school_params
      params.require(:school).permit(:name, :name_kana, :name_en, :summary, :station, :address, :hp, :facebook, :twitter, :instagram, :tiktok, :youtube, :condition, :anual_fee, :have_dormitory, :dormitory_fee, :is_open, :prefecture, :image, :image_from)
    end

    def tag_params
      params.require(:school).permit(:nationality)
    end

end

createアクション

input_tags = tag_params[:nationality].split(' ')
tag_paramsで取得した[:nationality]情報をスペースで区切って、タグの配列を作成。input_tagsにその配列を代入する。

school.create_tags(input_tags)
これはshcoolモデルで定義したメソッド。新しく入力したタグを、student_nationality_tagから探す/存在しない場合は作成する。

updateアクション

school.update_tags(input_tags)
これはschoolモデルで定義したメソッド。
registared_tags に登録済みのタグを代入。
new_tags に新しく入力されたタグを代入し、追加。
destroy_tags に登録済みのタグ - 新しく入力されたタグ を代入し、削除。

showアクション

ビューの記述

schools/new.html.erb
....
<div class="row mt-2">
      <div class="col-md-5 text-right">
        <%= f.label :学生の国籍 %>(半角スペースで区切ってください)
      </div>
      <div class="col-md-5 offset-md-1">
        <%= f.text_field :nationality, placeholder: "インドネシア" %>
      </div>
    </div>
....

schools/edit.html.erb
....
<div class="row mt-2">
      <div class="col-md-5 text-right">
        <%= f.label :学生の国籍 %>(半角スペースで区切ってください)
      </div>
      <div class="col-md-5 offset-md-1">
        <%= f.text_field :nationality, value: @school.student_nationality_tags.map(&:nationality).join(" "), class: "form-control" %>
      </div>
    </div>
....

学校情報編集画面では、登録済みのタグを半角スペース区切りで表示する。
@school.student_nationality_tags.map(&:nationality).join(" ")について、以下補足。

@school.student_nationality_tags
@schoolの持つすべてのstudent_nationality_tagsを取得。

.map(&:nationality)
@school.student_nationality_tagsのそれぞれからnationalityカラムのデータを取り出し、配列を作成。

.join(" ")
作成した配列をスペース区切りの1つの文字列にする。

schools/show.html.erb
....
<div class="row">
    <div class="col-md-3">
      <p>【学生の国籍】</p>
    </div>
    <div class="col-md-9">
      <ul class="list-unstyled">
        <%= @school.student_nationality_tags.map { |tag| tag.nationality }.join(", ") %>
      </ul>
    </div>
  </div>
....

<%= @school.student_nationality_tags.map { |tag| tag.nationality }.join(", ") %>この部分について、以下補足。

@school.student_nationality_tags
@schoolの持つすべてのstudent_nationality_tagsを取得。

.map { |tag| tag.nationality }
@school.student_nationality_tagsの各要素のnationalityカラムのデータを取り出して、配列を作成。

.join(", ")
作成した配列を,で区切って、つなげる。

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

https://qiita.com/ten__/items/1859c72e99400f7d1f9b

Discussion