はじめに
近年のSNSやブログ等では、1つの投稿に対して、複数のタグを付けられることが一般的になっています。
しかし、これらの機能の実装を丁寧に説明した記事はあまりないと思いました。
そこで、自分の復習も兼ねて具体例を交えながら細かく丁寧に解説するために、記事にまとめてみました。
実装環境
Ruby 3.1.2
Rails 6.1.6
アソシエーション
まず、今回の設定条件は以下にします。
- トピック(Topic)にタグ(Tag)をつける
- タグは自身で入力する仕様になっている
- 1つの記事には複数のタグを付けられる
- 1つのタグも複数の記事に使用される
以上より、Topicモデルの中にタグのカラムを設けるのではなく、TopicモデルとTagモデルを分けて作成します。
また、TopicモデルとTagモデルは、多対多の関係になります。
従って、中間テーブル(Tagging)を設ける必要があります。
モデルについて
まず、モデルを生成します。
rails g model Topic title:string
rails g model Tag name:string
rails g model Tagging topic:references tag:references
モデルを生成したら、各モデルファイルにアソシエーションを記述します。
Topicモデル
class Topic < ApplicationRecord
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
end
Tagモデル
class Tag < ApplicationRecord
has_many :taggings, dependent: :destroy
has_many :topics, through: :taggings
end
Taggingモデル
class Tagging < ApplicationRecord
belongs_to :topic
belongs_to :tag
end
最後にDBに反映も忘れずに行いましょう。
rails db:migarate
ルーティング
tagsのcreateも必要かと思うかもしれませんが、今回はtopicのcreateアクションの中で作成するため、記述していません。
Rails.application.routes.draw do
resources :topics, only: [:new, :create, :edit, :update]
end
コントローラ
今回は、Topicのコントローラだけ作成します。
rails g controller topics new edit
new, editのインスタンス変数を記述しておきます。
さらに、create, editアクションも追記しておきましょう。
class TopicsController < ApplicationController
def new
@topic = Topic.new
end
def create
end
def edit
@topic = Topic.find(params[:id])
end
def update
end
end
ビュー
次に、新規登録フォームと編集フォームを記述します。
タグについては、トピックの登録と同じフォームで入力する仕様にします。
また、タグは複数入力させるために、半角スペースを空けて入力してもらうようにします。(後のコントローラの設定次第で、カンマなどに指定することも可能です。)
<!-- 新規登録フォーム -->
<%= form_with model: Topic.new, url: topics_path do |f| %>
<%= f.label :title, "タイトル" %>
<%= f.text_field :title %>
<%= f.label :name, "タグ(半角スペースで複数個登録できます)" %>
<%= f.text_field :name %>
<%= f.submit "登録" %>
<% end %>
<%= form_with model: topic, url: topic_path(topic) do |f| %>
<%= f.label :title, "タイトル" %>
<%= f.text_field :title %>
<%= f.label :name, "タグ(半角スペースで複数個登録できます)" %>
<%= f.text_field :name %>
<%= f.submit "登録" %>
<% end %>
コントローラにアクションを追記
ここからがこの記事の本題です。
【create】
トピックに関しては、new.html.erbの入力フォームから受け取ったパラメータをnewの中に格納してsaveするといった一般的な方法になります。(createでもOK)
タグについては、まず、半角スペース入力してもらうので、split
メソッドで区切ります。
その後に、すでに入力されたタグが保存されている可能性があるため、find_or_create_by
メソッドを使用するといった流れです。
では、実装してみましょう。
※そのまま記述するとファットコントローラーになるため、モデルにメソッドを切り出すようにします。
class TopicsController < ApplicationController
...
def create
@topic = Topic.new(topic_params)
input_tags = tag_params.split # tag_paramsをsplitメソッドを用いて配列に変換する
@topic.create_tags(input_tags) # create_tagsはtopic.rbにメソッドを記載している
@topic.save
redirect_to topics_path # こちらはindexに遷移するように設定している
end
...
private
def topic_params # topicに関するストロングパラメータ
params.require(:topic).permit(:title)
end
def tag_params # tagに関するストロングパラメータ
params.require(:topic).permit(:name)
end
end
class Topic < ApplicationRecord
...
def create_tags(input_tags)
input_tags.each do |tag| # splitで分けたtagをeach文で取得する
new_tag = Tag.find_or_create_by(name: tag) # tagモデルに存在していれば、そのtagを使用し、なければ新規登録する
tags << new_tag # 登録するtopicのtagに紐づける(中間テーブルにも反映される)
end
end
end
具体例としては、
- 入力フォームのタグに
["食べ物 お寿司 東京"]
が入力されたとします - createアクションのsplitメソッドで
input_tag = ["食べ物","お寿司","東京"]
に分割します - each文でタグがすでに作成されているかを確認し、存在しなければ作成します
new_tag = "食べ物", new_tag = "お寿司", new_tag = "東京"
-
tags << new_tag
でtopicに紐づけます(この場合には、taggingは3つ作成されることになります)
【update】
次に更新するときのコードを記述します。
こちらの方が少々やっかいです。
全体的な流れとしては、まず、編集画面で入力されたタグが現在のと紐づいているタグなのかを判断します。
新しく入力されたタグであれば、タグを新規登録するとともにトピックに紐付けます。
削除されたタグの場合には、Taggingから該当のレコードを削除します。(Tagモデルからは削除しません。)
では、実装してみましょう。
class TopicsController < ApplicationController
...
def update
@topic.update(topic_params)
input_tags = tag_params.split
@topic.update_tags(input_tags) # udpate_tagsはtopic.rbに記述している
redirect_to request.referer # editページに戻るようにしている
end
...
private
def topic_params # topicに関するストロングパラメータ
params.require(:topic).permit(:title)
end
def tag_params # tagに関するストロングパラメータ
params.require(:topic).permit(:name)
end
end
class Topic < ApplicationRecord
...
def update_tags(input_tags)
registered_tags = tags.pluck(:name) # すでに紐付けれらているタグを配列化する
new_tags = input_tags - registered_tags # 追加されたタグ
destroy_tags = registered_tags - input_tags # 削除されたタグ
new_tags.each do |tag| # 新しいタグをモデルに追加
new_tag = Tag.find_or_create_by(name: tag)
tags << new_tag
end
destroy_tags.each do |tag| # 削除されたタグを中間テーブルから削除
tag_id = Tag.find_by(name: tag)
destroy_tagging = Tagging.find_by(tag_id: tag_id, topic_id: id)
destroy_tagging.destroy
end
end
end
具体例を使って説明します。
前提として、トピックに紐づけられてタグモデルに"食べ物", "お寿司", "東京"
が保存されているとします。
- 編集画面の入力フォームでタグに
["食べ物 お寿司 豪華"]
と入力されたとします
前提と比較して、"東京"
というタグが削除され、"豪華"
というタグが新しく入力されたことになります - パラメータを受け取ったら、splitメソッドで入力されたタグを分割します
input_tag = ["食べ物","お寿司","豪華"]
- 次に、モデルに記述されている
update_tags(input_tags)
に移り、tags.pluck(:name)
ですでに紐付けられているタグを配列化しなおします -
input_tags - registered_tags
で編集フォームで入力されたタグの配列 - すでに紐付けられているタグの配列
の演算をします
配列 - 配列では、前者の配列から後者の配列と重複しているものを削除するという演算になります
したがって、今回は["食べ物","お寿司","豪華"]-["食べ物 お寿司 東京"]
なので、new_tags = ["豪華"]
になります
これで、編集によって新しく入力されたタグだけを取り出すことができました - その逆も同じで、
registered_tags - input_tags
とすることで、削除されたタグを取り出すことができます
destroy_tags = ["東京"]
- その後は、
new_tags
では、each文を使用して、createのときと同じようにタグの検索または作成をして、トピックに紐付けます -
destroy_tags
では、タグ自体は削除せず、紐付けとなるTaggingモデルのレコードのみを削除します(タグは他のトピックでも使用されている可能性があるため)
each文でタグのidを探し、そのtag_idを使用してTaggingモデルから該当レコードを探し、削除するという流れです
最後に
以上で実装完了になります。
中間テーブルは多少ややこしく感じますが、適切に扱えると非常に有用であると感じます。
避けては通れない道なので、ちゃんと理解して活用していきたいですね。