🦄

Rails タグ付け機能

2023/06/13に公開

はじめに

ポートフォリオ作成中のプログラミング初学者です🔰

今日は複数のタグ付け&検索機能を実装しました!
もし間違いなどあればぜひ教えてください🥲

完成イメージ

レイアウトはまだあまり整えられておらず、見づらくてすみません!🥲

  • ⭐️タグを複数つけて投稿できる

  • ⭐️投稿一覧画面でタグの一覧と記事数を表示

  • ⭐️タグのリンクを押すと、そのタグがついている投稿を表示できる

  • ⭐️投稿詳細画面

テーブル定義

PostWorkout(トレーニング投稿):WorkoutTag(トレーニングタグ)=1:N
WorkoutTag(トレーニングタグ):PostWorkout(トレーニング投稿)=1:N

の多対多になるので、

中間テーブルとしてPostWorkoutTagを用意してそれぞれの外部キーを持たせています。

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

  • モデル作成
rails g model WorkoutTag                  
rails g model PostWorkoutTag

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

xxxxxxxx_create_post_workout_tags.rb
class CreatePostWorkoutTags < ActiveRecord::Migration[6.1]
  def change
    create_table :post_workout_tags do |t|
      t.references :post_workout, null: false, foreign_key: true
      t.references :workout_tag, null: false, foreign_key: true

      t.timestamps
    end
    # 同じタグは2回保存出来ない
    add_index :post_workout_tags, [:post_workout_id,:workout_tag_id],unique: true
  end
end
xxxxxxxx_create_workout_tags.rb
class CreateWorkoutTags < ActiveRecord::Migration[6.1]
  def change
    create_table :workout_tags do |t|
      t.string :name, null: false

      t.timestamps
    end
    add_index :workout_tags, :name, unique:true
  end
end

⬇️

rails db:migrate

モデルファイル

アソシエーションの設定

app/models/workout_tag.rb
class WorkoutTag < ApplicationRecord
  has_many :post_workout_tags, dependent: :destroy
  has_many :post_workouts, through: :post_workout_tags

  validates :name, presence:true, length:{maximum:50}
end

through: :post_workout_tagsは、2つのモデル間の関連がpost_workout_tagsモデルを通じて行われることを示しています。

app/models/post_workout_tag.rb
class PostWorkoutTag < ApplicationRecord
  belongs_to :post_workout
  belongs_to :workout_tag
end
app/models/post_workout.rb
class PostWorkout < ApplicationRecord
:
 # タグのリレーションのみ記載
  has_many :post_workout_tags, dependent: :destroy
  has_many :workout_tags, through: :post_workout_tagsend

コントローラー記述

app/controllers/public/post_workousts_controller.rb
class Public::PostWorkoutsController < ApplicationController
  def create
    @post_workout = PostWorkout.new(post_workout_params)
    @post_workout.end_user_id = current_end_user.id
     # 受け取った値を,で区切って配列にする
    tag_list = params[:post_workout][:name].split(',')
    if @post_workout.save
      @post_workout.save_workout_tags(tag_list)
      redirect_to post_workouts_path, notice:'投稿が完了しました'
    else
      render :new
    end
  end
end
  • ⭐️tag_list = params[:post_workout][:name].split(',')
    このコードは、params[:post_workout][:name]から取得した文字列を ,(コンマ)で分割し、分割された結果を配列に格納して tag_list という変数に代入しています。
split()メソッドとは

RubyのStringクラスのインスタンスメソッドで、文字列を特定の区切り文字で分割し、その結果を配列として返します。引数には区切り文字(デリミタ)を指定します。

例えば、split(',')というメソッドを呼び出すと、カンマ(,)を区切り文字として文字列を分割します。

str = "apple,banana,orange"
array = str.split(',')
# array => ["apple", "banana", "orange"]
  • ⭐️save_workout_tagsについては、モデルファイルで定義しています。
    タグで入力した値を配列に収めてる(tag_list)を引数としてモデルファイルに渡します。

モデルファイル(save_workout_tags)

app/models/post_workout.rb
class PostWorkout < ApplicationRecord
:
  def save_workout_tags(tags)
  # タグが存在していれば、タグの名前を配列として全て取得
    current_tags = self.workout_tags.pluck(:name) unless self.workout_tags.nil?
    # 現在取得したタグから送られてきたタグを除いてoldtagとする
    old_tags = current_tags - tags
    # 送信されてきたタグから現在存在するタグを除いたタグをnewとする
    new_tags = tags - current_tags

    # 古いタグを消す
    old_tags.each do |old_name|
      self.workout_tags.delete WorkoutTag.find_by(name:old_name)
    end

    # 新しいタグを保存
    new_tags.each do |new_name|
      workout_tag = WorkoutTag.find_or_create_by(name:new_name)
      self.workout_tags << workout_tag
    end
  endend
  • ⭐️current_tags = self.workout_tags.pluck(:name) unless self.workout_tags.nil?
    このコードは、
    「現在のインスタンスに関連付けられたタグが存在する場合には、そのタグの名前をcurrent_tagsに、リストとして取得する。タグが存在しない場合には何もしない」という意味です。
コードを分解してみる
  • self.tags: selfは現在のインスタンス(たとえば、特定のブログポストまたは特定の商品など)を参照します。self.tagsは、そのインスタンスに関連付けられたタグの集合を返します。

  • self.tags.pluck(:name): pluckメソッドは、指定したカラムの値を配列として取得するActiveRecordのメソッドです。ここではnameというカラム(タグの名前を表す)の値を取得します。したがって、self.tags.pluck(:name)は、現在のインスタンスに関連付けられたすべてのタグ名のリストを返します。
    pluckメソッドが便利な件について

  • unless self.tags.nil?: unlessはRubyの条件分岐の一つで、「〜でない限り」という意味です。つまり、self.tags.nil?がfalse(self.tagsがnilでない)の場合に、その後のコードが実行されます。これはself.tagsがnil(つまり、現在のインスタンスに関連付けられたタグが存在しない)場合には、タグ名を取得しようとするとエラーになるため、そのエラーを防ぐためのものです。

pluckとmapの違いを調査する

検索・タグの表示

ルーティング記述

  • 検索で使用するルーティングとコントローラーを記述します
config/routes.rb
# タグの検索で使用する
      get "search_tag" => "post_workouts#search_tag"

controller

app/controllers/public/post_workouts_controller.rb
class Public::PostWorkoutsController < ApplicationController
:
  def search_tag
    #検索結果画面でもタグ一覧表示
    @tag_list = WorkoutTag.all
     #検索されたタグを受け取る
    @tag = WorkoutTag.find(params[:workout_tag_id])
     #検索されたタグに紐づく投稿を表示
    @post_workouts = @tag.post_workouts
  endend

投稿一覧画面

  • タグを表示します!(seedなどでデータを先に作っておかないと表示されません!)

controller

app/controllers/public/post_workout_controller.rb
class Public::PostWorkoutsController < ApplicationController
  def index
    @post_workouts = PostWorkout.all
    @tag_list = WorkoutTag.all
  end
end

view

app/views/public/post_workouts/index.html.erb
<!--タグリスト-->
  <% @tag_list.each do |list|%>
    <i class="fa-sharp fa-solid fa-tag"></i>
    <%=link_to list.name,search_tag_path(workout_tag_id: list.id) %>
    <%="(#{list.post_workouts.count})" %>
  <% end %>

投稿詳細画面

controller

app/controllers/public/post_workout_controller.rb
class Public::PostWorkoutsController < ApplicationController
  def show
    @post_workout = PostWorkout.find(params[:id])
    @tag_list = @post_workout.workout_tags.pluck(:name).join(',')
    @post_workout_tags = @post_workout.workout_tags
  end
end

view

app/views/public/post_workouts/show.html.erb
<!-- タグリスト -->
      <% @post_workout_tags.each do |tag| %>
      <i class="fa-sharp fa-solid fa-tag"></i>
        <%= link_to tag.name,search_tag_path(workout_tag_id: tag.id) %>
      <% end %>

投稿編集画面

元々作っていたフィットネス投稿フォームの部分テンプレートに下記を足しました!

view

app/views/public/post_workouts/_form.html.erb
<%= form_with model: post_workout , local: true do |f| %>
:
	# ここを追加
        <label>タグ(,で区切ると複数タグ登録できます)</label>
        <%= f.text_field :name,value: @tag_list, class: 'form-control' %>
:
      <% if post_workout.new_record? %>
        <%= f.submit '新規作成', class: "btn btn-success mt-4" %>
      <% else %>
        <%= f.submit '更新する', class: "btn btn-success mt-4" %>
      <% end %><% end %>

value: @tag_listにしてあげないと、フォームに何も入ってない状態になってしまいます。

controller

app/controllers/public/post_workout_controller.rb
class Public::PostWorkoutsController < ApplicationController
:
  def edit
    @post_workout = PostWorkout.find(params[:id])
    @tag_list = @post_workout.workout_tags.pluck(:name).join(',')
  end
  
  def update
    @post_workout = PostWorkout.find(params[:id])
    tag_list=params[:post_workout][:name].split(',')
    if @post_workout.update(post_workout_params)
      @post_workout.save_workout_tags(tag_list)
      redirect_to post_workouts_path
    else
      render :edit
    end
  endend
  • ⭐️@tag_list = @post_workout.workout_tags.pluck(:name).join(',')
    このコードは、現在の@post_workout(特定のワークアウト投稿)に関連づけられた全てのworkout_tags(ワークアウトタグ)の名前を配列で取得し、それらをカンマ(,)でつなげた一つの文字列にして、その結果を@tag_listに格納しています。
    結果として、@tag_listには例えば "tag1,tag2,tag3" のような形式の文字列が格納されます。
joinメソッドとは

joinメソッドはRubyの配列(Array)クラスに定義されたメソッドの一つで、配列の要素を指定した区切り文字で結合し、一つの文字列を作成します。

  • 例えば、次のような配列があるとします:
array = ["apple", "banana", "cherry"]
  • joinメソッドを使用して、これらの要素をカンマで結合することができます:
joined_string = array.join(", ")
# これにより、joined_stringは "apple, banana, cherry" という文字列になります。

この場合、配列の各要素はカンマとスペース(", ")で結合され、一つの文字列が作成されます。

  • また、引数を指定しないでjoinメソッドを呼び出すと、配列の要素は何も挿入されずに結合されます:
joined_string = array.join
# これにより、joined_stringは "applebananacherry" という文字列になります。

この場合、配列の各要素は直接連結され、一つの文字列が作成されます。

検索結果の表示

view

app/views/public/post_workouts/search_tag.html.erb
<h2>タグが<%=@tag.name%>の投稿一覧</h2>

  <!--タグリスト-->
  <% @tag_list.each do |list|%>
    <i class="fa-sharp fa-solid fa-tag"></i>
    <%=link_to list.name,search_tag_path(workout_tag_id: list.id) %>
    <%="(#{list.post_workouts.count})" %>
  <% end %>

参考にさせていただいた記事🌱

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

https://qiita.com/tobita0000/items/daaf015fb98fb918b6b8#ビュー

https://qiita.com/MandoNarin/items/5a5610a40c66f77d6c10

さいごに

説明を省略してしまっているところもあるのですが、
この流れで実装できるはずです!
もし間違いや抜けているところなどあれば教えていただけますと幸いです🙇🏻‍♀️

Discussion