LoginSignup
10
8

More than 1 year has passed since last update.

【rails】タグ付け機能を解説していくよ!

Posted at

タグ付け機能を解説!

この記事でPFにタグ付け機能をつけたい方用に記事を書いていきます!

【機能】
・タグ付け機能を投稿につけたい

完成図はこんな感じ。

新規投稿画面:
image.png

投稿画面:
image.png

タグ付け機能は処理が少し難しいので理解するのに時間がかかりますが
ひとつずつ見ていけば理解できると思いますよ!

機能別に解説

それでは、実際のrailsのMVCモデルの流れに沿って
下記の順番に塔載をしていくとしますね!

➀ マイグレーションファイル
⓶ ルーティング
➂ モデル(アソシエーション)
⓸ コントローラー+モデルのメソッド(create)
⓹ コントローラー+モデルのメソッド(update)
⓺ コントローラー+モデルのメソッド(edit)
⓻ ビュー

モデルにはメソッドを記載していくので
その部分が若干難易度が高いです。

都度解説をいれていくので、なるべく
分かりやすく解説をしていきます!

それではレッツゴ!

ステップ⓵:マイグレーションファイル

まず初めにマイグレーションファイルから作成していきましょう!
今回はタグ付けは多対多の関係になるため中間テーブルを用意します。

必要になるテーブルは、

➀:投稿テーブル(投稿用テーブル)
⓶:タグの中間テーブル(投稿idとタグidを保存)
➂:タグテーブル(タグの名前)

ER図:
image.png

ER図で見ていただいてわかる通り、
3つのテーブルがアソシエーションで
つながることでタグづけ機能ができるようになります。

ここではテーブルのカラムだけを作成して
アソシエーションはモデルで定義していきます!

【こんな感じ】
投稿 <-> タグの中間テーブル <-> タグテーブル

さっそく、マイグレーションファイルをみていきますね!

php/db/migrate/日付_create_posts.rb

class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.integer :user_id, null: false
      t.date :date_on, null: false
      t.time :time_at, null: false
      t.string :place, null: false
      t.text :content, null: false
      t.string :title, null: false
      t.float :rate, null: false
      t.timestamps
    end
  end
end

php/db/migrate/日付_create_tag_relationships.rb

class CreateTagRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :tag_relationships do |t|
      t.references :post, foreign_key: true, null: false
      t.references :tag, foreign_key: true, null: false
      t.timestamps

      # referencesにすることでindexが自動付与
      # 外部キーには主キー+_idが必要だが、referecesの場合は不要
      # foreign_key:trueで外部キー制約が設定される

    end
  end
end

php/db/migrate/日付_create_tags.rb

class CreateTags < ActiveRecord::Migration[5.2]
  def change
    create_table :tags do |t|
      t.string :name, null: false
      t.timestamps
    end
  end
end

中間テーブルであるtag_relationshipテーブルは
post_idとtag_idの両方を保管するのですが、
referenceと記載をすることでuser_id、post_idと_idを
記載しなくてもOKになります。

しかも、referenceというのを使うとindexというので自動付与され
高速で検索できるようになるためタグ付けのように
データの検索が多い場合にはreferenceを使用するといいでしょう。
(個人レベルで使用するポートフォリオには、
 そこまで必要ないですが、大きいデータベースで
 何万とuserがいたりする場合にはindex処理があるとベター)

そして、referenceを使用するとforeign_key trueと記載すれば
自動で外部キー指定をしてくれるようになります。

外部キー指定というのは、よきせぬ値を登録したくないなど
といったようにデータの整合性を保つ時に使用したり
指定したカラムに登録をしたい場合に記載をします。

ステップ⓶:ルーティング

今回の場合はtagしたタグの一覧表示するページ等はないので
タグに関するルーティングは作成しません。

タグ機能は新規投稿の画面や投稿画面で表示するため
必要になるルーティングは、Postビューに必要になるので
Post系については事前に作成しているものとします。

ステップ⓷:モデル

次はモデルを記載していきます。

ここではアソシエーションをして各テーブルの紐づけをします!

php/models/post.rb

  # タグ付けのアソシエーション
  has_many :tag_relationships, dependent: :destroy
  has_many :tags, through: :tag_relationships

php/models/tag_relationship.rb

class TagRelationship < ApplicationRecord
  belongs_to :post
  belongs_to :tag
  # タグ付けのバリデーション・アソシエーション
  # validateをいれることで重複を防ぐ
  validates :post_id, presence: true
  validates :tag_id, presence: true
end

php/models/tag.rb

class Tag < ApplicationRecord
  # タグ付けのバリデーション・アソシエーション
  has_many :tag_relationships, dependent: :destroy
end

ここでの注目ポイントはpostモデルに記載をする
throughです。

今回は投稿画面でタグ付けを表示したいので
throughtを書くことで中間テーブルを通して
tagテーブルのタグのカラムを引っ張ってくることができます。

つまり、投稿画面でタグを表示したかったら
postの画面でtagsというアソシエーションを使うことで
投稿に紐づくタグの一覧を引っ張ってこれるんですね♪

ステップ⓸:コントローラー+モデルのメソッド記述(create偏)

次はコントローラーの記述と、モデルに各メソッドを記述します。

モデルに各メソッドというのは、モデルに自分で作成した処理のコードを
書くことで、それをコントローラーで引っ張ってこれるんですね!

そうすることでコードの可読性があがり、
コントローラーがスッキリとしたものになります。

php/app/controllers/posts_controller.rb

   def create
    @post = current_user.posts.new(post_params)
    #:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
    tags = params[:post][:tag_id].split(',')
    if @post.save
    #@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
      @post.save_tags(tags)
      redirect_to root_path, success: t('posts.create.create_success')
    else
      render :new
    end
  end

  def edit
    @post = Post.find(params[:id])
    @tags = @post.tags.pluck(:name).join(',')
  end

  def update
    @post = Post.find(params[:id])
    #:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
    tags = params[:post][:tag_id].split(',')
    if @post.update(post_params)
    #@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
      @post.update_tags(tags)
      redirect_to root_path, success: t('posts.edit.edit_success')
    else
      render :edit
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
    redirect_to root_path, success: t('posts.destroy.destroy_success')
  end

  private

  def post_params
    params.require(:post).permit(:user, :date_on, :time_at, :place, :title, :content, :rate, post_images_images: [])
  end
end

createアクションから、見ていくとします。

def create
@post = current_user.posts.new(post_params)
#:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
tags = params[:post][:tag_id].split(',')
if @post.save
#@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
@post.save_tags(tags)
redirect_to root_path, success: t('posts.create.create_success')
else
render :new
end
end

まず初めにおさえないといけないポイントは
何をテーブルに保存しないといけないかです。

中間テーブルには、post_idとtag_idを保存して
tagテーブルのnameには実際にタグ付けしたタグの名前を
データの処理の中で行わないといけません。

なので、tags = params[:post][:tag_id].split(',')
ここで行っていることとしては、
[:post]というのはform_withでデータがとばされるときの
パラメーターです。

postのパラメーターの中にさらにいろいろな
データが入っていて、投稿の内容、投稿の画像...
そして今回保存したいので投稿に紐づいているタグの名前です。

なので、postパラメーターの中に入っている
tag_idを取得してあげましょう。
(この中には、タグ付けに使った名前が入っています。)

.split(',')このメソッドは配列にいれるメソッドです。
配列というのは[a,b,c]こんなような形のものです。

複数タグがある場合は[足,背中,腹筋]のように
箱の中に配列として保存されるんですね。

そしてif @post.saveなので、
投稿が保存されたら下のメソッドが走って
中間テーブルとタグテーブルに保存がされます。

実際にタグテーブルに保存をしている箇所は、
@post.save_tags(tags)で行っています。
※save_tagsはモデルに書いたメソッドを呼んでいます。

それでは、どんなメソッドをモデルに書いているか見ていきます。

php/app/models/post_model.rb

 # タグ付けの新規投稿用メソッド
  def save_tags(tags)
    tags.each do |new_tags|
      # selfは明示的に記載していてこの場合だとコントローラーの@postになる
      # tag_relationshipsがthroughしているのでtagsでアソシエーションを指定すると中間テーブルを通過した際に保存される。
      self.tags.find_or_create_by(name: new_tags)
    end
  end

  # タグ付けの更新用メソッド
  def update_tags(latest_tags)
    if self.tags.empty?
      # 既存のタグがなかったら追加だけ行う
      latest_tags.each do |latest_tag|
        self.tags.find_or_create_by(name: latest_tag)
      end
    elsif latest_tags.empty?
      # 更新対象のタグがなかったら既存のタグをすべて削除
      # 既に保存がされていたら既に登録されているタグの内容を削除
      self.tags.each do |tag|
        self.tags.delete(tag)
      end
    else
      # 既存のタグも更新対象のタグもある場合は差分更新
      current_tags = self.tags.pluck(:name)
      #左を起点に引き算をする
      #       b             a b c
      old_tags = current_tags - latest_tags
      #一致したものを取り出す
      # a c       a b c            b 
      new_tags = latest_tags - current_tags

      # a  c
      old_tags.each do |old_tag|
        tag = self.tags.find_by(name: old_tag)
        self.tags.delete(tag) if tag.present?
      end


      new_tags.each do |new_tag|
        # b
        self.tags.find_or_create_by(name: new_tag)
        # self.tags << new_tags
      end
    end
  end

# タグ付けの新規投稿用メソッド
def save_tags(tags)
tags.each do |new_tags|
# selfは明示的に記載していてこの場合だとコントローラーの@postになる
# tag_relationshipsがthroughしているのでtagsでアソシエーションを指定すると中間テーブルを通過した際に保存される。
self.tags.find_or_create_by(name: new_tags)
end
end

このメソッドがコントローラーに書いた@post.save_tags(tags)と
つながっているわけですね。

save_tagsで何をやっているかというと
まずコントローラーから受け取った引数(tags)を
save_tags(tags)で受け取ります。

ここには実際にタグ付けしたタグの名前が入っています。
そのタグをeach文で回して一つずつnew_tagsにいれていきます。

そしてタグの名前をself.tags.find_or_create_by(name: new_tags)
保存していきます。

selfは@post(postモデルの情報を含んだ状態で)tagsでアソシエーションをします。
(selfがわからない方は
https://qiita.com/smallisland-ken/items/4b8954bdc7823a3c9b57
こちらの記事を参考にしてみてください)

タグテーブルにこれから登録されるタグの名前がなかったら
タグテーブルにあるnameカラムにeachで回したnew_tagsを保存していきます。

中間テーブルを通しているので、中間テーブルを通過した際に
post_idが保存され、タグテーブルに保存されるとtag_idも決まるので
そしたら中間テーブルにもtag_idが保存されます。

ここまでがcreateの処理になります!

ステップ⓹:コントローラー+モデルのメソッド記述(update偏)

次に見ていくのはタグ付けのアップデート偏になります。

アップデートは少し複雑になりますが
一つずつみていけば大丈夫なので安心してくださいね!

php/app/controllers/posts_controller.rb

   def create
    @post = current_user.posts.new(post_params)
    #:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
    tags = params[:post][:tag_id].split(',')
    if @post.save
    #@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
      @post.save_tags(tags)
      redirect_to root_path, success: t('posts.create.create_success')
    else
      render :new
    end
  end

  def edit
    @post = Post.find(params[:id])
    @tags = @post.tags.pluck(:name).join(',')
  end

  def update
    @post = Post.find(params[:id])
    #:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
    tags = params[:post][:tag_id].split(',')
    if @post.update(post_params)
    #@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
      @post.update_tags(tags)
      redirect_to root_path, success: t('posts.edit.edit_success')
    else
      render :edit
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
    redirect_to root_path, success: t('posts.destroy.destroy_success')
  end

  private

  def post_params
    params.require(:post).permit(:user, :date_on, :time_at, :place, :title, :content, :rate, post_images_images: [])
  end
end

def update
@post = Post.find(params[:id])
#:postはpostで投稿されてきた際にパラメーターとして飛ばされ、その中の[:tag_id]を取得して、splitで,区切りにしている
tags = params[:post][:tag_id].split(',')
if @post.update(post_params)
#@postをつけることpostモデルの情報を.save_tagsに引き渡してメソッドを走らせることができる
@post.update_tags(tags)
redirect_to root_path, success: t('posts.edit.edit_success')
else
render :edit
end
end

updateをみていくとしましょう。
アップデートもcreateと考え方は同じです。
@post.update_tags(tags)で保存するのですが
アップデートする時にupdate_tagsをメソッドで呼び出します。

php/app/models/post_model.rb

 # タグ付けの新規投稿用メソッド
  def save_tags(tags)
    tags.each do |new_tags|
      # selfは明示的に記載していてこの場合だとコントローラーの@postになる
      # tag_relationshipsがthroughしているのでtagsでアソシエーションを指定すると中間テーブルを通過した際に保存される。
      self.tags.find_or_create_by(name: new_tags)
    end
  end

  # タグ付けの更新用メソッド
  def update_tags(latest_tags)
    if self.tags.empty?
      # 既存のタグがなかったら追加だけ行う
      latest_tags.each do |latest_tag|
        self.tags.find_or_create_by(name: latest_tag)
      end
    elsif latest_tags.empty?
      # 更新対象のタグがなかったら既存のタグをすべて削除
      # 既に保存がされていたら既に登録されているタグの内容を削除
      self.tags.each do |tag|
        self.tags.delete(tag)
      end
    else
      # 既存のタグも更新対象のタグもある場合は差分更新
      current_tags = self.tags.pluck(:name)
      #左を起点に引き算をする
      #       b             a b c
      old_tags = current_tags - latest_tags
      #一致したものを取り出す
      # a c       a b c            b 
      new_tags = latest_tags - current_tags

      # a  c
      old_tags.each do |old_tag|
        tag = self.tags.find_by(name: old_tag)
        self.tags.delete(tag) if tag.present?
      end


      new_tags.each do |new_tag|
        # b
        self.tags.find_or_create_by(name: new_tag)
        # self.tags << new_tags
      end
    end
  end

if self.tags.empty?
# 既存のタグがなかったら追加だけ行う
latest_tags.each do |latest_tag|
self.tags.find_or_create_by(name: latest_tag)
end

ここで行っているのはアップデート処理をする際に
これからアップデートしようとしているタグテーブルが空だったら
それを登録するということを行います。

つまり初回の投稿ではタグを登録していなくて
2回目でタグ付けをつけた場合を想定した処理となります。

ちなみにfind_or_create_byというのは、
探してなければ保存というメソッドになります。

elsif latest_tags.empty?
# 更新対象のタグがなかったら既存のタグをすべて削除
# 既に保存がされていたら既に登録されているタグの内容を削除
self.tags.each do |tag|
self.tags.delete(tag)
end

latest_tags.empty?をみていきましょう。
これについては、これから登録しようとするタグが
空だった場合にテーブルのタグ名を削除という処理になります。
つまりどういうことかというと、
もともとabcとはいっていて、これからアップデートするタグは
何もない、つまり空でタグをアップデートした場合
abcのタグは削除します。

テーブルからアップデートすることで
タグを消したいときとかを考えこの処理をいれています。

else
  # 既存のタグも更新対象のタグもある場合は差分更新
  current_tags = self.tags.pluck(:name)
  #左を起点に引き算をする
  #       b            a b c
  old_tags = current_tags - latest_tags
  #一致したものを取り出す
  # a c       a b c            b 
  new_tags = latest_tags - current_tags

  # a  c
  old_tags.each do |old_tag|
    tag = self.tags.find_by(name: old_tag)
    self.tags.delete(tag) if tag.present?
  end


  new_tags.each do |new_tag|
    # b
    self.tags.find_or_create_by(name: new_tag)
    # self.tags << new_tags
  end
end

current_tags = self.tags.pluck(:name)では、
現在selfに紐づいているpost_idでtagsを使いアソシエーションをします。

これで@post=selfのタグの名前をすべて引っ張ってきてcurrent_tagsに格納します。

old_tags = current_tags - latest_tags
old_tagsは古いタグの名前を削除するためにここで変数を削除します。

ここでポイントなのは左を起点に引き算をするということで
数学的な考えの引き算とは違うので注意しましょう。

例えばcurrent_tags(現在テーブルに登録されているタグ)にbがはいっているとします
そして、latest_tags(これから登録しようとしているタグ)にはabcがはいっていると仮定して
b - abcになるので、abが残るのでは?と普通は考えると思いますが、

そうではなくて、左側にbがあり右にもabcとbがあるので
b同士が引き算され、左のbがなくなります。

すると左側にはなにも残らないのでold_tagsには何もない状態になります。
ここが混乱するポイントですが左の結果だけを偏すに代入すると覚えてください。

tag = self.tags.find_by(name: old_tag)
self.tags.delete(tag) if tag.present?

そして、ここではold_tagを削除しています。
b - abcであればなにも残らないので何も削除されません。
abc - bであれば aとcが削除されます。

これで古いタグは削除完了します。

  new_tags = latest_tags - current_tags

  new_tags.each do |new_tag|
    # b
    self.tags.find_or_create_by(name: new_tag)
  end

最後にここをみていきましょう。

ここでは新しいタグを保存する作業を行います。

latest_tags - current_tagsで
これからabc - 現在 cがあればaとcが新たにテーブルに保存されます。

ここまででアップデートの処理は完了となります。

ステップ⓺:コントローラー(edit偏)

次に編集したい画面を表示する場合のコントローラーの記述をします。

def edit
@post = Post.find(params[:id])
@tags = @post.tags.pluck(:name).join(',')
end

新規投稿やアップデートと違い簡単です。

@post.tagsでアソシエーションでpost_idに紐づいたtag_idをすべて持ってきて
joinで文字列に連携をします。

こうすることで@tagsをviewで使用することでタグ一覧を
カンマで分けて表示することができます!

ちなみにdeleteをしたい場合はアソシエーションで
dependent destroyにしているので投稿自体をすればいいので
タグもすべて削除されます。

def destroy
post = Post.find(params[:id])
post.destroy
redirect_to root_path, success: t('posts.destroy.destroy_success')
end

なので↑の処理をすれば削除されますよ♪

ステップ⓻:ビュー作成

最後にタグ付けの名前を表示したいので
コントローラーのインスタンス変数をviewで記述しましょう!

/php/posts_controller.rb

def show
    @post = Post.find(params[:id])
    @comment = Comment.new
    # コメント用のコード
    @post_comments = @post.comments

    # タグ用のコード
    @tags = @post.tags.pluck(:name).join(',')

    # 閲覧数表示
    impressionist(@post, nil, :unique => ["session_hash"])
  end
/php/app/views/posts/show.html.erb

 <%= @tags %>

使い方としてはこんな感じ。
好きなところにコントローラーで作成したインスタンス変数を貼り付けてください。

そしてeditの場合もshowと同じようにeditコントローラーで作成した
インスタンス変数をviewに貼り付けてください!

こうすることで完成です!

まとめ

いかがでしたでしょうか?

タグ付け機能を説明させていただきました♪

難しいところは特にモデルのメソッドの部分になりますが
一つ一つみていけば理解できると思うので
何回も復習してみてくださいね!

それでは、チャオ!

10
8
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
10
8