はじめに
現在作成しているアプリでタグ機能を実装したのでその実装方法を残しておきます。
Railsにはタグ機能の実装を簡単にしてくれる acts-as-taggable-on というgemがありますが、関連付けの練習も踏まえて自前で実装します。
現在作っているアプリのPK,FKのみを示したER図は以下のようになります。
実行環境
この記事は以下の環境で動作確認しています。
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
複合キーインデックスを張ります。
こうすることにより、同じタグを二回保存できないようにします。
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
にします。
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モデルでも同じです。
タグ名はユニークで必ず保持していて欲しいので以下のようなバリデーションにします。
class Tag < ApplicationRecord
has_many :tag_relationships, dependent: :destroy
has_many :profiles, through: :tag_relationships
validates :name, uniqueness: true, presence: true
end
class TagRelationship < ApplicationRecord
belongs_to :profile
belongs_to :tag
validates :tag_id, presence: true
validates :profile_id, presence: true
end
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]でパラメーターを受け取れるようにします。
<div class="input-field col s12">
<i class="material-icons prefix">local_offer</i>
<%= f.text_field :tag, placeholder: "タグを複数つけるには' , 'で区切ってください" %>
</div>
表示する際はeachで配列で保存されているタグを繰り返し表示します。
<% @user_profile.tags.each do |tag| %>
<div class="chip">
<%= tag.name %>
<i class="close material-icons">close</i>
</div>
<% end %>
コントローラーの作成
ユーザーとプロフィールはhas_one
を用いて一対一の関係にしているので、buildする際は「インスタンス名.build_アソシエーション名」としています。
プロフィール情報と一緒に送られてきたタグを保存できるようにします。
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メソッドは下記に示します。
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の一部を抜粋します。
<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(',')で取得した配列を「,」で区切った文字列にします。
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メソッドを更新でも使えるようにします。
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を使わずに実装した際のメモ