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

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

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

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

# 完成物

![スクリーンショット 2024-06-12 20.31.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/f0553e44-cd77-2f8b-d937-caea74328b09.png)

![スクリーンショット 2024-06-12 20.34.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/2445ee6b-7007-1b0a-83e1-77dd37c4b2a8.png)

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


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

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

![スクリーンショット 2024-06-12 20.39.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/0348288f-5695-7f04-824a-399053f84403.png)

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

#### モデル作成
投稿のPlanモデルはすでに作成済みですので、
タグとタグプランを作成します

```
$ rails g model Tag
$ rails g model PlanTag plan:references tag:references
```
:::note info
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
```

:::note info
解説 🌱
・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 ||= []
```
    
<details><summary>selfとは</summary>

:::note info
(直訳：自己、自身)
現在のオブジェクト自身のこと
selfを使用したオブジェクト自身のメソッドや属性にアクセスできる
今回の場合はPlanのインスタンスを指します

詳しく説明すると、selfを使用して作成された 'save_plan_tags'メソッドをPlansコントローラーなどで定義した場合、そのメソッドを呼びたしている'Plan'オブジェクトを指します

:::
</details>

<details><summary>pluckメソッドとは</summary>

:::note info
ActiveRecordのクエリメソッドの一つ
特定のカラムの値を直接データベースから取得し、配列として返します
単一カラム、複数カラム、条件指定での取得全て可能
```
# 単一カラム
names = User.pluck(:name)

# 複数カラム
user_info = User.pluck(:name, :email)

# 条件指定
names = User.where("age >= 30").pluck(:name)

```
:::
</details>
<details><summary>current_tags ||= []とは</summary>

:::note info
この記述は’self.tags.pluck(:name)’で取得した値が、nilまたはfalseだった場合に、
current_tagsに空白を代入するという意味です
nilでもfalseでもない場合はcurrent_tagsに値が代入されます
:::

</details>

```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
```

<details><summary>tags.each do |tag_name|とは</summary>

:::note info
この記述で'tags'配列内の各タグ名 (tag_name) に対してループを実行する

:::

</details>

<details><summary>find_or_create_by()とは</summary>

:::note info
()の中のタグ名がすでに存在するかデータベースで検索し、
存在しなければ新しく作成するメソッド
findやfind_byとの違いはこちらの記事にまとめています
https://qiita.com/3rarara/items/98d3e11b344c3938addf
:::

</details>

<details><summary>tag_name.stripとは</summary>

:::note info
タグ名の前後の空白を取り除くメソッドです。
例えば　" tag "　などの場合、tag前後の余分な空白が削除されます
:::

</details>

<details><summary>self.tagsとは</summary>

:::note info
Planオブジェクトに関連付けられたすべての Tagオブジェクトのこと
:::

</details>

<details><summary>self.tags.include?(tag)とは</summary>

:::note info
Planオブジェクトにそのタグが関連付けられているかをチェックする
すでに関連付けられている場合は true、そうでない場合は false を返します
:::

</details>

</details>

<details><summary>self.tags << tagとは</summary>

:::note info
 タグを Planオブジェクトに関連付けます
 ただし、unless self.tags.include?(tag) によって、
 すでに関連付けられているタグを再度関連付けることを防いでいます
:::

</details>

```plan.rb
# 削除されたタグの削除
tags_to_delete = current_tags - tags
self.tags.where(name: tags_to_delete).destroy_all
```

プラン投稿編集時、タグを削除してプランを保存した場合にタグを削除します
例）初回投稿時のタグ：旅行, デート, 大阪
　　投稿編集時のタグ：旅行, 大阪
この場合、「デート」というタグを削除する必要があります
<details><summary>tags_to_delete = current_tags - tags</summary>

:::note info
current_tags = self.tags.pluck(:name)で代入したcurrent_tagsから
フォームで送信された変更後のtagsを引きます

```plan.rb
# ["旅行","デート","大阪"] - ["旅行","大阪"] = ["デート"]
    current_tags  -   tags   =   tags_to_delete
```
🌱 削除対象のデートを　tags_to_deleteに代入しています
:::

</details>

最後に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)
    # タグの情報が存在するかの確認
    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
```

ここに注目！⚠️
```plans_controller.rb
plan_tags = params[:plan][:tags].split(',') if params[:plan][:tags]
```

<details><summary>params[:plan][:tags]とは</summary>

:::note info
フォームから送信されたデータを含むハッシュで、
Planのタグ情報を含む文字列のこと

その後の、if params[:plan][:tags]という記述で
params[:plan][:tags] が nilまたは空でないかを確認する
:::

</details>

<details><summary>.split(',')とは</summary>

:::note info
フォームから送信された文字列をカンマ(',')で分割する

例）"旅行,大阪,デート" → ["旅行", "大阪", "デート"] と変換される
:::

</details>

🌱 条件付き代入
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(", ")
```

<details><summary>@plan.tags.map(&:name)とは</summary>

:::note info
@plan.tags の各タグオブジェクトの name 属性を取得し、
それを配列として返す。&:name は、tag.name を簡潔に表現したもの
:::

</details>

<details><summary>.join(", ")とは</summary>

:::note info
map メソッドによって得られたタグ名の配列をカンマで区切った文字列に結合
例）["旅行", "大阪", "デート"] →  "旅行,大阪,デート"　と変換される
:::

</details>

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

```plans_controller.rb
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
```

ここに注目！⚠️

```plans_controller.rb
@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編
#### コントローラーにアクションを記述

```plans_controller.rb
class Public::PlansController < ApplicationController

  def show
    @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のタグのロゴを使用しています！<img width="20" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782860/687dd100-3dea-d1da-f8c4-e7ef7ce68b44.png">
不要であれば削除してください！


# 検索機能実装

#### ルーティング設定
```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
    # タグ一覧
    @tags = Tag.all
    # 検索されたタグ
    @tag = Tag.find(params[:tag_id])
    # 検索されたタグに紐づく投稿一覧
    @plans = @tag.plans
  end
end
```

```search_tag.html.erb
<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 %>
```

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

# 参考にした記事
参考にさせていただきました！ありがとうございました！

https://zenn.dev/goldsaya/articles/625bdf837c1c68#%E6%A4%9C%E7%B4%A2%E3%83%BB%E3%82%BF%E3%82%B0%E3%81%AE%E8%A1%A8%E7%A4%BA

https://qiita.com/mmaumtjgj/items/cdc76572d392957c4299