43
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-08-02

やりたいこと

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

:fist:複数タグの投稿
スクリーンショット 2021-08-02 14.02.51.png
:fist:投稿一覧画面でタグの一覧と、記事数の表示
スクリーンショット 2021-08-02 14.02.12.png
:fist:タグを押すと、検索ができ、そのタグがついてる投稿を検索できる
スクリーンショット 2021-08-02 14.02.26.png
:fist:投稿詳細画面
スクリーンショット 2021-08-02 14.01.08.png

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

①テーブル定義

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

スクリーンショット 2021-08-02 14.10.25.png

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

モデル作成

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

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

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

      t.timestamps
    end
  end
end
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
    # 同じタグを2回保存するのは出来ないようになる
    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して、続いてモデルファイルを記載します。

モデルファイル

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〜を見たときはなんのこっちゃでしたが、、だいぶ理解できるようになってきました!
中間テーブルは、”仲介役”という風に考えるようにしてから、何だかわかるようになってきた気がします。

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つけてないので。

post.rb
  # タグのリレーションのみ記載
  has_many :post_tags,dependent: :destroy
  has_many :tags,through: :post_tags
ここからが難しかった・・
何書いてあるかわからない:sweat:ので1行ずつ見ていきます。

①コントローラー記述

```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..確か分割して配列を作ってくれるようやつだったように思います。

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

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

①問題のモデルファイル

ここがすごく難しい。。。
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に、「タグの名前を配列として」全て取得します。

「plunk」とは?  
簡単にいうとカラムの中身を展開してくれる

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

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:」については「これは新しいタグ!」と判断されています。
モデルファイルは、なんだか気高い?裁判所のような?イメージです。

①タグの表示・viewとコントローラー

タグを表示していきます。(リンクは未実装です)

投稿一覧画面

postのコントローラー
def index
    @posts = Post.page(params[:page]).per(10)
    @tag_list=Tag.all
  end
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 %>では取り出せないんだろうな。とは理解しました。

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

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

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

スクリーンショット 2021-08-02 18.28.59.png

詳細画面での表示

posts_controller.rb
def show
    @post = Post.find(params[:id])
    @post_comment=PostComment.new
    @post_tags = @post.tags
end
posts/show.html.erb
<% @post_tags.each do |tag| %>
  <%=tag.name%><%="(#{tag.posts.count})" %><% end %>

①編集機能 忘れてました..

  <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 にしてあげないと、フォームに何も入ってない状態になってしまいます。

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:注意!!このままだと、投稿編集時にタグを減らしたりするとエラーになります。

ひとまず終わり

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

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

とても参考にさせていただきました!

43
47
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
43
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?