[Rails] タグ機能/タグ一覧ページの実装

# はじめに
タグ機能をgemを使わず、実装するための忘備録です。
設計は、中間テーブルを使うToxi法、といわれる形式で作成します。

# 実行環境
- Rails 7.0.4.1
- Ruby 3.0.4
- Devise 4.8.1

# 実装
### 1. アソシエーション
![タグ機能.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3118527/ffe07c55-d7d6-7846-7247-1ba43c8615c7.png)
articlesテーブルとtagsテーブルを多対多でつなげます。
こうすることで、tagの編集をしたいときに、それぞれのarticleを編集する必要がなくなります。

注意する点は、articlesテーブルとtagsテーブルは親子関係にはなっていないことです。
そのため、modelに dependent: :destroyとつけても articleの削除と同時に、tagは削除されません。
>構造を考えれば、当たり前なのですが、articleを削除してもtagが消えず、理解するまでに時間がかかりました。

また、中間テーブルの命名は、つなげるテーブルの名前 + Relationships (TagRelationships) のようにしているものが多く見られました。


>今回は後で、タグとは別にカテゴリー機能を追加する際にも同じテーブルを使いたいと思い、ArticleRelationshipsとしました。

追記：
交差テーブルには関連の意味を表す名前をつけた方がいいようなので今回の場合、Taggingsとする方が良いようです。

### 2. モデル

#### 2.1 モデルの作成
```
rails g model tag name:string
rails g model article_relationship article:references tag:references
```
マイグレーションファイルに追記をします。
```ruby: db/migrate
class CreateTags < ActiveRecord::Migration[7.0]
  def change
    create_table :tags do |t|
      t.string :name, null: false

      t.timestamps
    end
    add_index :tags, :name, unique: true
  end
end
```
modelのバリデーションで、presense: trueとすると、
railsから実行にはnullが使えなくなりますが、まだ、SQLの実行ではnullで保存ができてしまうようです。
そこで、migrateで null: false とすることで、SQLからの実行でもnullの保存ができなくなります。~~また、このテーブルはタグのidと名前があるだけなので、indexをつける必要はないでしょう。~~
indexは検索時間を高速化するためるためだけではなく、重複を防ぐためにも使うため、今回はindexを書く必要があります。


```ruby:db/migrate
class CreateArticleRelationships < ActiveRecord::Migration[7.0]
  def change
    create_table :article_relationships do |t|
      t.references :article, null: false, foreign_key: true
      t.references :tag, null: false, foreign_key: true

      t.timestamps
    end
    add_index :article_relationships, [:article_id, :tag_id], unique: true
  end
end
```
unique: true としたことで、同じ名前のタグの保存をできないようにしています。

データベースへ反映させます。
```
rails db:migrate
```

#### 2.2 モデルの関連付け
```ruby: app/model/tag.rb
class Tag < ApplicationRecord
  has_many :article_relationships, dependent: :destroy
  has_many :articles, through: :article_relationships
  validates :name, presence: true, uniqueness: true
end
```
```ruby: app/model/article_relationship.rb
class ArticleRelationship < ApplicationRecord
  belongs_to :article
  belongs_to :tag
  validates :tag_id, uniqueness: { scope: :article_id }
end

```
```ruby: app/model/article.rb
class Article < ApplicationRecord
  .
  .
  has_many :article_relationships, dependent: :destroy
  has_many :tags, through: :article_relationships
  .
  .
end
```
validates :name, uniqueness: trueでタグの名前が重複する保存を防ぎます。

### 3. Viewの作成
```app/views/articles/new
   <%= form_with(model: @article) do |f| %>
    .
      <%= f.text_field :tag_ids, class: "form-control", id:'tag_ids',\
        placeholder: "タグをつける。複数つけるには','で区切ってください。" %>
    .
   <% end %>
```

```app/views/articles/_articl.html.erb
    <% article.tags.each do |tag| %>
       <%= tag.name %>
    <% end %>
```

### 4. コントローラーの作成

```ruby:app/model/articles.rb
 def save_tags(savearticle_tags)
    savearticle_tags.each do |new_name|
      article_tag = Tag.find_or_create_by(name: new_name)
      self.tags << article_tag
    end
 end
```
タグを追加できるようにします。
「find_or_create_by」メソッドはカラムの中から同じ値がないか探して、あればそのままfindの動き、なければcreateの動きで新たにカラムに保存します。

```ruby: app/controllers/articles_controller.rb
  def create
    @article = current_user.articles.build(article_params)
    tag_list = params[:article][:tag_ids].split(',')  #追記
    @article.image.attach(params[:article][:image])
    if @article.save
      @article.save_tags(tag_list)  #追記
      flash[:success] = "Article created!"
      redirect_to root_url
    else
      render 'new', status: :unprocessable_entity
    end
  end
```
tag_listとして、タグ一覧を取得します。複数タグが入力されている場合、文字列として送られてくるタグをsplit(',')で分割して配列にします。
@aritcle.save_tags(tag_list)でタグをarticleに関連づけて保存します。

### 5.　タグ一覧ページを作る
これで、タグの追加ができるようになりました。
ここからは、追加されたタグの一覧を表示し、そのタグをもつ記事に飛べるようにしたいと思います。

#### 5.1 アクションを作成
```ruby: app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
 .
 .
  def index
    @articles = params[:tag_id].present? ? Tag.find(params[:tag_id]).articles : Article.all
  end
 .
 .
  def tags
    @tags = Tag.all
  end
  .
end
  
```
<foo>@</foo>articles = params[:tag_id].present? ? Tag.find(params[:tag_id]).articles : Article.all は、

params[:tag_id]があるかを確認し、
あればtag_idを使ってタグを検索して指定のタグをもつ記事を、なければ、全ての記事を@articleへ格納しています。

このコードにより、 タグ一覧ページ作成に必要なことはほとんど可能になっているので、あとはルーティングとviewを変更するだけです。

#### 5.1 ルーティングとビュー

```ruby: config/routes.rb
resources :articles do
  get :tags, on: :collection
end
```
:collectionとルーティングを行うことで、/articles/tagsでタグの一覧を取得することができます。
```app/views/articles/index.html.erb
<% provide(:title, "記事一覧") %>
<div>
  <h2>記事一覧</h2>
    <% @articles.each do |article| %>
      <%= article.title %><br>
    <% end %>
</div>
```
ここの<foo>@</foo>ariticlesは先ほど宣言したものです。
/articlesへ直接アクセスすると、<foo>@</foo>articlesにはparams[:tag_id]がないため、全てのariticleが表示されます。

```app/views/articles/tags.html.erb
<% provide(:title, "タグ一覧") %>
<div>
  <h2>タグ一覧</h2>
    <% @tags.each do |t| %>
      <%= link_to t.name, articles_path(tag_id: t.id)%>
    <% end %>
</div>
```
/article/tags　にはタグの名前をさせ、tag_idをつかって、指定したariricleのページに飛ぶようにしています。
以上で、タグ一覧ページ完成となります。

# 参考にさせていただいた記事
https://qiita.com/you8/items/b2394104c6f9865f5d46
https://qiita.com/E6YOteYPzmFGfOD/items/bfffe8c3b31555acd51d
https://qiita.com/E6YOteYPzmFGfOD/items/177f18e706df05f9b42e