[Ruby on rails]タグ付け機能① 複数のタグ付け

<h2>やりたいこと</h2>
①投稿に対して複数タグを付けれるようにする
②タグをリンクにして、検索できるようにする。リンクになってるタグを押すと、
そのタグを付与してる記事一覧が表示される

:fist:複数タグの投稿
![スクリーンショット 2021-08-02 14.02.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/d4dbb3f1-e343-c84e-9bf0-62e85deb1d82.png)
:fist:投稿一覧画面でタグの一覧と、記事数の表示
![スクリーンショット 2021-08-02 14.02.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/8445fd36-0244-02c4-bc88-0fc446c80fa4.png)
:fist:タグを押すと、検索ができ、そのタグがついてる投稿を検索できる
![スクリーンショット 2021-08-02 14.02.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/e537a70b-a6f6-99a1-8b3f-15fcb4f31851.png)
:fist:投稿詳細画面
![スクリーンショット 2021-08-02 14.01.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/62bdc75a-5019-7494-d230-af43f0b7a8fa.png)

タグを一個だけ付与するのは、別記事で投稿しました。
複数タグの実装はかなり難しかったのでまとめていきます。

https://qiita.com/ki_87/items/7eaec2f710b5bb2c0527

<h2>①テーブル定義</h2>

一応、以下の感じです。
タグ：投稿＝1:N（複数）
投稿：タグ＝１：N（複数）　の多　対　多　になるので、
中間テーブルであるPostTagを用意して、それぞれの外部キーを持たせてます。

![スクリーンショット 2021-08-02 14.10.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/81d9b550-740f-29b2-d0ba-1c1dfcbb5e06.png)


<h2>①モデル/マイグレーションファイル作成</h2>

モデル作成

```
rails g model Tag name:string                    
rails g model PostTag post:references tag:references
```

<h4>マイグレーションファイル</h4>

```rb:tagのマイグレーションファイル
class CreateTags < ActiveRecord::Migration[5.2]
  def change
    create_table :tags do |t|
      t.string :name,null: false

      t.timestamps
    end
  end
end
```

```rb:post_tagsのマイグレーションファイル
class CreatePostTags < ActiveRecord::Migration[5.2]
  def change
    create_table :post_tags do |t|
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
    # 同じタグを２回保存するのは出来ないようになる
    add_index :post_tags, [:post_id, :tag_id], unique: true
  end
end
```

```add_index :post_tags, [:post_id, :tag_id], unique: true```
これは、複合キーインデックスで、この記述で同じタグを2回保存できないようにしています。

ここで、```rails db:migrate```して、続いてモデルファイルを記載します。

<h4>モデルファイル</h4>


```perl:tag.rb
class Tag < ApplicationRecord
  has_many :post_tags,dependent: :destroy, foreign_key: 'tag_id'
  # タグは複数の投稿を持つ　それは、post_tagsを通じて参照できる
  has_many :posts,through: :post_tags
  
  validates :name, uniqueness: true, presence: true
end
```

初めて```through〜```を見たときはなんのこっちゃでしたが、、だいぶ理解できるようになってきました！
中間テーブルは、”仲介役”という風に考えるようにしてから、何だかわかるようになってきた気がします。


```perl:post_tag.rb
class PostTag < ApplicationRecord
  belongs_to :post
  belongs_to :tag
  validates :post_id, presence: true
  validates :tag_id, presence: true
end
```

validates〜＿のところはPostとTagの関係を構築する際、2つの外部キーが存在することは絶対なので、
バリデーションを貼るそうですが、無くても問題ない気がしています（by初心者）
これまでの中間テーブルでもvalidationつけてないので。


```rb:post.rb
  # タグのリレーションのみ記載
  has_many :post_tags,dependent: :destroy
  has_many :tags,through: :post_tags
```


<h6>ここからが難しかった・・</h6>
何書いてあるかわからない:sweat:ので１行ずつ見ていきます。

<h2>①コントローラー記述</h2>
```rb:post_controller.rb
def create
    @post = Post.new(post_params)
    @post.user_id=current_user.id
    # 受け取った値を,で区切って配列にする
    tag_list=params[:post][:name].split(',')
    if @post.save
      @post.save_tag(tag_list)
      redirect_to posts_path(@post),notice:'投稿完了しました:)'
    else
      render:new
    end
```


:star:tag_list=params[:post][:name].split(',')
なんかpaizaで勉強した記憶があるsplit..確か分割して配列を作ってくれるようやつだったように思います。

<h6>「splitとは」</h6>
<h6>1番目の引数に指定したパターンに従って文字列を分割し、分割された各部分文字列を要素とする配列を取得します。</h6>

:star:@post.save_tag(tag_list)
save_tagなんてそんなメソッドがあるのか・・・と思っていましたが違います！！！！！
@post.saveの後に書いてあるから、紛らわしいですね・・
save_tagの働きについては、モデルファイルで定義してます。
タグで入力した値を配列に収めてる（tag_list)を引数としてモデルファイルに渡します。

<h2>①問題のモデルファイル</h2>

<h6>ここがすごく難しい。。。</h6>

```rb
def save_tag(sent_tags)
  # タグが存在していれば、タグの名前を配列として全て取得
    current_tags = self.tags.pluck(:name) unless self.tags.nil?
    # 現在取得したタグから送られてきたタグを除いてoldtagとする
    old_tags = current_tags - sent_tags
    # 送信されてきたタグから現在存在するタグを除いたタグをnewとする
    new_tags = sent_tags - current_tags
    
    # 古いタグを消す
    old_tags.each do |old|
      self.tags.delete　Tag.find_by(name: old)
    end
    
    # 新しいタグを保存
    new_tags.each do |new|
      new_post_tag = Tag.find_or_create_by(name: new)
      self.tags << new_post_tag
   end
end
```
```current_tags = self.tags.pluck(:name) unless self.tags.nil?```
unless~は「タグが存在してるか？」を確認しています。
※投稿フォームでタグが入力されたか？を聞いてるわけではないので注意です。（ややこしいですが）
タグが存在している場合には、current_tagsに、「タグの名前を配列として」全て取得します。

<details><summary>「plunk」とは？　　</summary><div>
簡単にいうとカラムの中身を展開してくれる


https://qiita.com/k-o-u/items/31e4a2f9f5d2a3c7867f


</div></details>

```old_tags = current_tags - sent_tags```ここでは、「今あるタグ」から「新たに送られてきたタグ」を引いて、
「old_tag」に代入しています。
例えば、既に
「ネズミ:mouse:」
「お花:cherry_blossom:」
「コアラ:koala:」
というタグが存在していて、新たに、
「お花:cherry_blossom:」
「ユニコーン:unicorn:」
というタグが登録されたら
old_tagには、「ネズミ:mouse:」「コアラ:koala:」が入ります。

```new_tags = sent_tags - current_tags```
ここで、new_tagsに「ユニコーン:unicorn:」が入ります。

```old_tags.each do |old|〜```で古いタグを消します。
今回の投稿記事付与したタグは
「お花:cherry_blossom:」、「ユニコーン:unicorn:」です。

old_tagsに入ってる
「ネズミ:mouse:」「コアラ:koala:」には用がありませんのでサヨナラします。


```new_tags.each do |new|〜```で新しいユニコーンというタグを保管します。

（以下は自分なりの解釈ですので気にしないでください。）
モデルファイルを通過した時点で、手元に残ったタグは、
「お花:cherry_blossom:」、「ユニコーン:unicorn:」です。
さらに、「ユニコーン:unicorn:」については「これは新しいタグ！」と判断されています。
モデルファイルは、なんだか気高い？裁判所のような？イメージです。

<h2>①タグの表示・viewとコントローラー</h2>

タグを表示していきます。（リンクは未実装です）
<h4>投稿一覧画面</h4>

```rb:postのコントローラー
def index
    @posts = Post.page(params[:page]).per(10)
    @tag_list=Tag.all
  end
```

```erb:app/views/posts/index.html.erb
<!--タグリスト-->
<% @tag_list.each do |list|%>
<%=list.name %>
<%="(#{list.posts.count})" %><% end %>

投稿一覧画面で投稿に紐づくタグを表示
<% @posts.each do |post| %>
<i class="fas fa-tag"><%= post.tags.map(&:name).join(', ') %></i>

```
```<%= post.tags.map(&:name).join(', ') %>```の部分、
最初、<%= post.tags.name %>にしてたのですが、エラーになってしまいました。
確かに、タグは複数ついてるので、<%= post.tags.name %>では取り出せないんだろうな。とは理解しました。

<h6>mapとは？？&:とは？？joinとは？？</h6>
```<%= post.tags.map(&:name).join(', ') %>```
現状、postにタグは複数ついていて、配列になってます。['お花','ゴリラ','カエル']みたいな。
さて、これを取り出して、表示させたい。
```map(&:name)```は、配列の要素１つ１つを:nameの型にする　というような意味のようです。
例を見るとわかりやすいです。
例：['a' ,'b'].map(&:upcase) #=> ["A", "B"]

なので今回は、```map(&:name)```の部分で["お花","ゴリラ","カエル"]と文字列にされてるのだと思います。：nameはstring型にしてるので。

そして```join(', ')```指定した文字で区切って連結してくれます。
"お花,ゴリラ,カエル"となってくれるわけですね:eyes::fist:

![スクリーンショット 2021-08-02 18.28.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633266/ae049f28-093e-4e9f-ef20-258255a2e518.png)

<h4>詳細画面での表示</h4>

```rb:posts_controller.rb 
def show
    @post = Post.find(params[:id])
    @post_comment=PostComment.new
    @post_tags = @post.tags
end
```

```rb:posts/show.html.erb
<% @post_tags.each do |tag| %>
  <%=tag.name%><%="(#{tag.posts.count})" %><% end %>
```

<h2>①編集機能　忘れてました..</h2>

```erb
  <div class="field">
    <%= f.label"タグ (,で区切ると複数タグ登録できます)" %>
    <%= f.text_field :name,value: @tag_list,class:"form-control"%>
  </div>

　<div class="actions" style="margin:20px 0;">
    <%= f.submit "投稿",class:"btn btn-outline-primary btn-block" %>
  </div>
  <% end %>
```
```value: @tag_list ```にしてあげないと、フォームに何も入ってない状態になってしまいます。


```rb:posts_controller.rb
def edit
    @post = Post.find(params[:id])
    # pluckはmapと同じ意味です！！
    @tag_list=@post.tags.pluck(:name).join(',')
  end




  def update
    @post = Post.find(params[:id])
    tag_list=params[:post][:name].split(',')
    if @post.update(post_params)
       @post.save_tag(tag_list)
       redirect_to post_path(@post.id),notice:'投稿完了しました:)'
    else
      render:edit
    end
  end
```

:helmet_with_cross:<strong>注意！！このままだと、投稿編集時にタグを減らしたりするとエラーになります。</strong>

<h2>ひとまず終わり</h2>
今のままでは、投稿編集時にタグを減らすとエラーがでますので、
その対応については③の記事で説明します。

https://qiita.com/ki_87/items/b7ceded18d473dc22f8f

タグをリンクにして、投稿を取り出す手順については②の記事で説明します！

https://qiita.com/ki_87/items/ae5d44af096df01ca587



<h2>とても参考にさせていただきました！</h2>

https://qiita.com/kurawo___D/items/e9a2dd0bf46cca706e5c

https://qiita.com/mkmk0mmk0/items/62bdba50f4f64c2d4d7e