はじめに
今日は複数のタグ付けと検索機能を実装しました
思ったより苦戦してしまったので、まとめます✍️
いろんな実装方法があるようですが、簡潔になるよう頑張りました!
※旅行やデートのプランを共有するアプリなので、投稿機能はPostではなくPlanを使用しています!
完成物
レイアウトがまだ整ってなくてすみません💦
ER図
プランは複数のタグをつけることができる
タグは複数のプランにつけることができる
ということでN:Nの関係が成り立ってしまうので、
中間テーブルにプランタグを使用します↓
モデル、マイグレーションファイル作成
モデル作成
投稿のPlanモデルはすでに作成済みですので、
タグとタグプランを作成します
$ rails g model Tag
$ rails g model PlanTag plan:references tag:references
referencesとは(直訳:参照、参考)
名前の通り、作成済みのテーブルを参照する場合に使用します
コマンドを実行すると下記カラムがマイグレーションファイルに自動追加されます
t.references :plan, null: false, foreign_key: true
t.references :tag, null: false, foreign_key: true
ここで指定しなくとも、後からマイグレーションファイルに追加可能です!
のちにモデルに記載する「belongs_to」を自動で記載もしてくれます
※「has_many」は追加してくれないので注意⚠️
参考URL:https://prograshi.com/framework/rails/references_and_foreign-key/
マイグレーションファイル
class CreateTags < ActiveRecord::Migration[6.1]
def change
create_table :tags do |t|
t.string :name, null: false
t.timestamps
end
add_index :tags, :name, unique: true
end
end
class CreatePlanTags < ActiveRecord::Migration[6.1]
def change
create_table :plan_tags do |t|
t.references :plan, null: false, foreign_key: true
t.references :tag, null: false, foreign_key: true
t.timestamps
end
add_index :plan_tags, [:plan_id,:tag_id], unique: true
end
end
マイグレーションファイルの記述が完了したら忘れないうちにmigrateしましょう!
$ rails db:migrate
解説 🌱
・referencesを指定することでindexが自動付与され、高速検索が可能になる
・referencesを指定することで外部キーxx_idが不要になる
・foreign_key:trueを指定することで外部キーとして設定される
・add_index :〜の記述で同じタグは保存できないようにしている
モデル(アソシエーション設定)
class Plan < ApplicationRecord
:
has_many :plan_tags, dependent: :destroy
has_many :tags, through: :plan_tags
#throughで、中間テーブルであるplan_tagsを通してtagのカラムを使用できる
:
end
class PlanTag < ApplicationRecord
belongs_to :plan
belongs_to :tag
end
class Tag < ApplicationRecord
has_many :plan_tags, dependent: :destroy
has_many :plans, through: :plan_tags
#throughで、中間テーブルであるplan_tagsを通してplanのカラムを使用できる
validates :name, presence: true, length: {maximum:20}
end
モデルにメソッドを記述
class Plan < ApplicationRecord
:
has_many :plan_tags, dependent: :destroy
has_many :tags, through: :plan_tags
:
# タグ機能
def save_plan_tags(tags)
# 現在のタグを取得し、nilの場合は空の配列を代入する
current_tags = self.tags.pluck(:name)
current_tags ||= []
# 新しいタグの作成・更新
tags.each do |tag_name|
tag = Tag.find_or_create_by(name: tag_name.strip)
self.tags << tag unless self.tags.include?(tag)
end
# 削除されたタグの削除
tags_to_delete = current_tags - tags
self.tags.where(name: tags_to_delete).destroy_all
end
end
一つづつ分解していきます!
長くなるので項目ごとにまとめています!
# 現在のタグを取得し、nilの場合は空の配列を代入する
current_tags = self.tags.pluck(:name)
current_tags ||= []
selfとは
(直訳:自己、自身)
現在のオブジェクト自身のこと
selfを使用したオブジェクト自身のメソッドや属性にアクセスできる
今回の場合はPlanのインスタンスを指します
詳しく説明すると、selfを使用して作成された 'save_plan_tags'メソッドをPlansコントローラーなどで定義した場合、そのメソッドを呼びたしている'Plan'オブジェクトを指します
pluckメソッドとは
ActiveRecordのクエリメソッドの一つ
特定のカラムの値を直接データベースから取得し、配列として返します
単一カラム、複数カラム、条件指定での取得全て可能
# 単一カラム
names = User.pluck(:name)
# 複数カラム
user_info = User.pluck(:name, :email)
# 条件指定
names = User.where("age >= 30").pluck(:name)
current_tags ||= []とは
この記述は’self.tags.pluck(:name)’で取得した値が、nilまたはfalseだった場合に、
current_tagsに空白を代入するという意味です
nilでもfalseでもない場合はcurrent_tagsに値が代入されます
# 新しいタグの作成・更新
tags.each do |tag_name|
tag = Tag.find_or_create_by(name: tag_name.strip)
self.tags << tag unless self.tags.include?(tag)
end
tags.each do |tag_name|とは
この記述で'tags'配列内の各タグ名 (tag_name) に対してループを実行する
find_or_create_by()とは
()の中のタグ名がすでに存在するかデータベースで検索し、
存在しなければ新しく作成するメソッド
findやfind_byとの違いはこちらの記事にまとめています
https://qiita.com/3rarara/items/98d3e11b344c3938addf
tag_name.stripとは
タグ名の前後の空白を取り除くメソッドです。
例えば " tag " などの場合、tag前後の余分な空白が削除されます
self.tagsとは
Planオブジェクトに関連付けられたすべての Tagオブジェクトのこと
self.tags.include?(tag)とは
Planオブジェクトにそのタグが関連付けられているかをチェックする
すでに関連付けられている場合は true、そうでない場合は false を返します
self.tags << tagとは
タグを Planオブジェクトに関連付けます
ただし、unless self.tags.include?(tag) によって、
すでに関連付けられているタグを再度関連付けることを防いでいます
# 削除されたタグの削除
tags_to_delete = current_tags - tags
self.tags.where(name: tags_to_delete).destroy_all
プラン投稿編集時、タグを削除してプランを保存した場合にタグを削除します
例)初回投稿時のタグ:旅行, デート, 大阪
投稿編集時のタグ:旅行, 大阪
この場合、「デート」というタグを削除する必要があります
tags_to_delete = current_tags - tags
current_tags = self.tags.pluck(:name)で代入したcurrent_tagsから
フォームで送信された変更後のtagsを引きます
# ["旅行","デート","大阪"] - ["旅行","大阪"] = ["デート"]
current_tags - tags = tags_to_delete
🌱 削除対象のデートを tags_to_deleteに代入しています
最後にwhereで先ほどのtags_to_deleteを検索し、
destroy_allメソッドを使用し全て削除しています!
Create編
コントローラーにアクションを記述
モデルに記述したメソッドを使用してアクションを記述していきます(やっと…!!!)
必要のない箇所は省略してますのでご留意ください
class Public::PlansController < ApplicationController
def new
@plan = Plan.new
end
def create
@plan = current_user.plans.new(plan_params)
# タグの情報が存在するかの確認
plan_tags = params[:plan][:tags].split(',') if params[:plan][:tags]
if @plan.save
@plan.save_plan_tags(plan_tags)
redirect_to plan_path(@plan)
else
render 'new'
end
end
end
ここに注目!⚠️
plan_tags = params[:plan][:tags].split(',') if params[:plan][:tags]
params[:plan][:tags]とは
フォームから送信されたデータを含むハッシュで、
Planのタグ情報を含む文字列のこと
その後の、if params[:plan][:tags]という記述で
params[:plan][:tags] が nilまたは空でないかを確認する
.split(',')とは
フォームから送信された文字列をカンマ(',')で分割する
例)"旅行,大阪,デート" → ["旅行", "大阪", "デート"] と変換される
🌱 条件付き代入
tag_list = ... if params[:plan][:tags] という構造により、
params[:plan][:tags] が存在する場合にのみ、tag_list が設定される
存在しない場合は、tag_list は設定されない
View(form)
わかりやすくするため簡素化しています!カスタマイズしてください!
<%= form_with model: @plan do |f| %>
<label>プラン</label>
<%= f.text_field :title %>
<%= f.text_area :body %>
:
<label>タグ</label>
<%= f.text_field :tags, value: @plan.tags.map(&:name).join(", ") %>
:
<%= f.submit '送信' %>
<% end %>
ここに注目!⚠️
edit画面で同じフォームを使用する場合、
valueの記載がないともとデータが反映されません!
value: @plan.tags.map(&:name).join(", ")
@plan.tags.map(&:name)とは
@plan.tags の各タグオブジェクトの name 属性を取得し、
それを配列として返す。&:name は、tag.name を簡潔に表現したもの
.join(", ")とは
map メソッドによって得られたタグ名の配列をカンマで区切った文字列に結合
例)["旅行", "大阪", "デート"] → "旅行,大阪,デート" と変換される
Update編
コントローラーにアクションを記述
class Public::PlansController < ApplicationController
def edit
@plan_tags = @plan.tags.pluck(:name).join(',')
end
def update
plan_tags = params[:plan][:tags].split(',') if params[:plan][:tags]
if @plan.update(plan_params)
@plan.tags.destroy_all
@plan.save_plan_tags(plan_tags)
redirect_to plan_path(@plan)
else
render 'edit'
end
end
end
ここに注目!⚠️
@plan.tags.destroy_all
@plan.save_plan_tags(plan_tags)
@plan.tags.destroy_allは、更新前の既存のタグを全て削除している
@plan.save_plan_tags(tag_list)は、フォームから受け取った新しいタグを関連付ける
この記述により、タグの減少、増加どちらにも対応できる!
View(form)
<%= form_with model: @plan, url: plan_path(@plan), method: :patch do |f| %>
<label>プラン</label>
<%= f.text_field :title %>
<%= f.text_area :body %>
:
<label>タグ</label>
<%= f.text_field :tags, value: @plan.tags.map(&:name).join(", ") %>
:
<%= f.submit '送信' %>
<% end %>
Show Index編
コントローラーにアクションを記述
class Public::PlansController < ApplicationController
def show
@plan_tags = @plan.tags
end
def index
@plans = Plan.all
@tags = Tag.all
end
end
View(show)
<% @plan_tags.each do |tag| %>
<div class="col text-center mx-4">
<i class="fa-sharp fa-solid fa-tag"></i>
<%= link_to tag.name, search_tag_path(tag_id: tag.id) %>
</div>
<% end %>
View(index)
<% plans.each do |plan| %>
<% plan.@tags.each do |tag| %>
<i class="fa-sharp fa-solid fa-tag"></i>
<%= link_to tag.name, search_tag_path(tag_id: tag.id) %>
<% end %>
<% end %>
Awesomeのタグのロゴを使用しています!
不要であれば削除してください!
検索機能実装
ルーティング設定
Rails.application.routes.draw do
:
scope module: :public do
:
resources :plans
get "search_tag" => "plans#search_tag"
end
:
end
Prefix Verb URI Pattern Controller#Action
search_tag GET /search_tag(.:format) public/plans#search_tag
コントローラーにアクションを記述
class Public::PlansController < ApplicationController
def search_tag
# タグ一覧
@tags = Tag.all
# 検索されたタグ
@tag = Tag.find(params[:tag_id])
# 検索されたタグに紐づく投稿一覧
@plans = @tag.plans
end
end
<h2>タグが<%= @tag.name %>の投稿一覧</h2>
<!--タグリスト-->
<% @tags.each do |tag| %>
<i class="fa-sharp fa-solid fa-tag"></i>
<%=link_to tag.name ,search_tag_path(tag_id: tag.id) %>
<%="(#{tag.plans.count})" %>
<% end %>
<!--一覧のテンプレート使用-->
<%= render 'index', plans: @plans, tags: @tags %>
さいごに
タグ機能作成の一助となりましたら幸いです
引き続きポートフォリオ制作頑張ります!
参考にした記事
参考にさせていただきました!ありがとうございました!