LoginSignup
2
0

タグ付け タグ検索 機能実装

Last updated at Posted at 2024-06-12

はじめに

今日は複数のタグ付けと検索機能を実装しました
思ったより苦戦してしまったので、まとめます✍️

いろんな実装方法があるようですが、簡潔になるよう頑張りました!

※旅行やデートのプランを共有するアプリなので、投稿機能はPostではなくPlanを使用しています!

完成物

スクリーンショット 2024-06-12 20.31.09.png

スクリーンショット 2024-06-12 20.34.28.png

レイアウトがまだ整ってなくてすみません💦

ER図

プランは複数のタグをつけることができる
タグは複数のプランにつけることができる

ということでN:Nの関係が成り立ってしまうので、
中間テーブルにプランタグを使用します↓

スクリーンショット 2024-06-12 20.39.01.png

モデル、マイグレーションファイル作成

モデル作成

投稿の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/

マイグレーションファイル

xxxxx_create_tags.rb
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
xxxxx_create_plan_tags.rb
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 :〜の記述で同じタグは保存できないようにしている

モデル(アソシエーション設定)

plan.rb
class Plan < ApplicationRecord
:
  has_many :plan_tags, dependent: :destroy
  has_many :tags, through: :plan_tags
  #throughで、中間テーブルであるplan_tagsを通してtagのカラムを使用できる
:
end
plan_tag.rb
class PlanTag < ApplicationRecord
  belongs_to :plan
  belongs_to :tag
end
tag.rb
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

モデルにメソッドを記述

plan.rb
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

一つづつ分解していきます!
長くなるので項目ごとにまとめています!

plan.rb
# 現在のタグを取得し、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に値が代入されます

plan.rb
# 新しいタグの作成・更新
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) によって、
すでに関連付けられているタグを再度関連付けることを防いでいます

plan.rb
# 削除されたタグの削除
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を引きます

plan.rb
# ["旅行","デート","大阪"] - ["旅行","大阪"] = ["デート"]
    current_tags  -   tags   =   tags_to_delete

🌱 削除対象のデートを tags_to_deleteに代入しています

最後にwhereで先ほどのtags_to_deleteを検索し、
destroy_allメソッドを使用し全て削除しています!

Create編

コントローラーにアクションを記述

モデルに記述したメソッドを使用してアクションを記述していきます(やっと…!!!)
必要のない箇所は省略してますのでご留意ください

plans_controller.rb
class Public::PlansController < ApplicationController

  def new
    @plan = Plan.new
  end

  def create
    @plan = current_user.plans.new(plan_params)
    # タグの情報が存在するかの確認
    tag_list = params[:plan][:tags].split(',') if params[:plan][:tags]
    if @plan.save
      @plan.save_plan_tags(tag_list)
      redirect_to plan_path(@plan)
    else
      render 'new'
    end
  end
end

ここに注目!⚠️

plans_controller.rb
tag_list = 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編

コントローラーにアクションを記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def edit
    @tag_list = @plan.tags.pluck(:name).join(',')
  end

  def update
    tag_list = params[:plan][:tags].split(',') if params[:plan][:tags]
    if @plan.update(plan_params)
      @plan.tags.destroy_all
      @plan.save_plan_tags(tag_list)
      redirect_to plan_path(@plan)
    else
      render 'edit'
    end
  end
end

ここに注目!⚠️

plans_controller.rb
@plan.tags.destroy_all
@plan.save_plan_tags(tag_list)

@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編

コントローラーにアクションを記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def show
    @tag_list = @plan.tags.pluck(:name).join(',')
    @plan_tags = @plan.tags
  end
  
  def index
    @plans = Plan.all
    @tags = Tag.all
  end
end

View(show)

show.html.erb
<% @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のタグのロゴを使用しています!
不要であれば削除してください!

検索機能実装

ルーティング設定

routes.rb
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

コントローラーにアクションを記述

plans_controller.rb
class Public::PlansController < ApplicationController

  def search_tag
    # タグ一覧
    @tag_list = Tag.all
    # 検索されたタグ
    @tag = Tag.find(params[:tag_id])
    # 検索されたタグに紐づく投稿一覧
    @plans = @tag.plans
  end
end
search_tag.html.erb
<h2>タグが<%= @tag.name %>の投稿一覧</h2>

<!--タグリスト-->
<% @tag_list.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 %>

さいごに

タグ機能作成の一助となりましたら幸いです
引き続きポートフォリオ制作頑張ります!

参考にした記事

参考にさせていただきました!ありがとうございました!

2
0
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
2
0