LoginSignup
13
14

More than 3 years have passed since last update.

Railsでタグ機能を実装

Last updated at Posted at 2020-11-25

はじめに

現在作成しているアプリでタグ機能を実装したのでその実装方法を残しておきます。
Railsにはタグ機能の実装を簡単にしてくれる acts-as-taggable-on というgemがありますが、関連付けの練習も踏まえて自前で実装します。
現在作っているアプリのPK,FKのみを示したER図は以下のようになります。
スクリーンショット 2020-11-25 9.00.20.png

実行環境

この記事は以下の環境で動作確認しています。
ruby 2.7.1
rails 6.0.3
DB MySQL

モデルの作成

Profileモデルは既に作ってあるという前提で進めていきます。
まず、tagモデルとtag_relastionshipモデルを作成します。

$ rails g model tag name:string
$ rails g model tag_relationship profile:references tag:references

複合キーインデックスを張ります。
こうすることにより、同じタグを二回保存できないようにします。

XXXXXXXXXXXXXX_create_tag_relationships.rb
class CreateTagRelationships < ActiveRecord::Migration[6.0]
  def change
    create_table :tag_relationships do |t|
      t.references :profile, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
    add_index :tag_relationships, [:profile_id, :tag_id], unique: true
  end
end

タグ名は必ず入力して欲しいのでnull:falseにします。

XXXXXXXXXXXXXX_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

モデルの関連付けとバリデーション

基本的な中間テーブルを用いる多対多の実装です。

中間デーブル経由でタグに紐付くprofileの情報を取得できるように has_many throughも定義します。これに関してはprofileモデルでも同じです。
タグ名はユニークで必ず保持していて欲しいので以下のようなバリデーションにします。

tag.rb
class Tag < ApplicationRecord
  has_many :tag_relationships, dependent: :destroy
  has_many :profiles, through: :tag_relationships

  validates :name, uniqueness: true, presence: true
end
tag_relationship.rb
class TagRelationship < ApplicationRecord
  belongs_to :profile
  belongs_to :tag

  validates :tag_id, presence: true
  validates :profile_id, presence: true
end
profile.rb
class Profile < ApplicationRecord
  belongs_to :user
  has_many :tag_relationships, dependent: :destroy
  has_many :tags, through: :tag_relationships
end

viewの作成

現在作成しているアプリではプロフィールの新規登録の際にタグも登録して欲しいのでprofiles/new.html.erbで実装します。
タグの部分だけ抜粋して載せます。

f.text_field :tagとすることでparams[:profile][:tag]でパラメーターを受け取れるようにします。

profiles/new.html.erb
<div class="input-field col s12">
  <i class="material-icons prefix">local_offer</i>
  <%= f.text_field :tag, placeholder: "タグを複数つけるには' , 'で区切ってください" %>
</div>

表示する際はeachで配列で保存されているタグを繰り返し表示します。

profiles/show.html.erb
<% @user_profile.tags.each do |tag| %>
  <div class="chip">
    <%= tag.name %>
    <i class="close material-icons">close</i>
  </div>
<% end %>

コントローラーの作成

ユーザーとプロフィールはhas_oneを用いて一対一の関係にしているので、buildする際は「インスタンス名.build_アソシエーション名」としています。

プロフィール情報と一緒に送られてきたタグを保存できるようにします。

profiles_controller.rb
  def new
    @user_profile = current_user.build_profile
  end

  def create
    @user_profile = current_user.build_profile(profile_params) # profile_paramsはストロングパラメーター 
    tag_list = params[:profile][:tag].split(',') # viewでカンマ区切りで入力してもらうことで、入力された値をsplit(',')で配列にしている。
    if @user_profile.save
      @user_profile.save_tags(tag_list) # save_tagsというインスタンスメソッドを使って保存している。
      flash[:notice] = "プロフィールの設定が完了しました"
      redirect_to root_url
    else
      render 'new'
    end
  end

save_tagsメソッドは下記に示します。

profile.rb
  def save_tags(profile_tag)
    profile_tag.each do |tag|
      new_tag = Tag.find_or_create_by(name: tag)
      self.tags << new_tag
    end
  end

「find_or_create_by」メソッドは引数で指定した値があればそれを取得し、なければ作成します。名前の通り、findかcreateするメソッドです。
self.tags << profile_tag ではプロフィールに関連したタグの配列に新たなタグを追加しています。
<< だけでなく、pushメソッドを使っても同じように要素を追加することができます。

タグの編集機能

プロフィールを編集する際にタグも変更できるようにします。

viewの一部を抜粋します。

profiles/edit.html.erb
<div class="input-field col s12">
  <i class="material-icons prefix">local_offer</i>
  <%= f.text_field :tag, value: @tag_list, placeholder: "タグを複数つけるには' , 'で区切ってください" %>
</div>

value: @tag_listとすることで既存の値を表示します。

editアクションではviewで既存の値を表示するために@tag_listを記述します。
pluck関数を使うことによってレシーバのカラムを簡単に取得します。
今回は@user_profile.tags.pluck(:name)としているのでプロフィールに関連したタグのnameカラムを配列で取得します。
join(',')で取得した配列を「,」で区切った文字列にします。

profiles_controller.rb
  def edit
    @user = Profile.find(params[:id]).user
    @user_profile = @user.profile
    @tag_list = @user_profile.tags.pluck(:name).join(',')
  end

  def update
    @user = Profile.find(params[:id]).user
    @user_profile = @user.profile
    tag_list = params[:profile][:tag].split(',')
    if @user_profile.update(profile_params)
      @user_profile.save_tags(tag_list)
      flash[:notice] = "プロフィールの変更が完了しました"
      redirect_to root_url
    else
      render 'edit'
    end
  end

今回は繰り返しの処理が単純なのでpluck関数を使わなくても「&:メソッド」を使えばタグの名前を同程度の記述量で取得することができます。
@user_profile.tags.map(&:name).join(',')
しかし、今回の場合、pluckは指定したカラムのみをSQLで取ってくるのでmapより早いと思われます。(間違っていたらご指摘ください。。)なのでこのままpluckを使用します。

save_tagsメソッドを更新でも使えるようにします。

profile.rb
  def save_tags(profile_tag)
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    old_tags = current_tags - profile_tag
    new_tags = profile_tag - current_tags

    # 古いタグを削除
    old_tags.each do |old_tag|
      self.tags.delete(Tag.find_by(name: old_tag))
    end

    # 新しいタグを追加
    new_tags.each do |new_tag|
      add_tag = Tag.find_or_create_by(name: new_tag)
      self.tags << add_tag
    end
  end

文字列の配列でも下記のように引き算できます。
a = ["first", "second", "third"]
b = ["first", "third", "forth"]
a - b => ["second"]
b - a => ["forth"]
これを用いて古いタグ、新しいタグを分けてそれぞれ処理します。

最後に

これでタグの作成、編集機能は完成です。
まだユーザーに取って良い形とは言えないのでjsを使うなどしてユーザーにとって使いやすいものに改善していこうと思います。

参考

pluckメソッドが便利な件について
pluckとmapの違いを調査する
Railsでタグ機能をgemを使わずに実装した際のメモ

13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14