LoginSignup
2
6

More than 3 years have passed since last update.

railsでタグ付け機能を実装する方法(投稿から編集まで)

Posted at

開発環境

Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系

前提

  • 投稿機能は実装済み(今回の例ではpostモデル)
  • 編集機能は実装済み

  • 手法として今回はFormオブジェクトモデルで実装します。(1つのフォームからpostsテーブルとtagsテーブルに保存する実装を目指します)

以下が今回登場する各テーブルです。

postsテーブル

Column Type Options
title string null: false
explanation text
category_id integer null: false
animal_name string
user references null: false, foreign_key: true

Association

  • belongs_to :user
  • has_many :comments
  • has_many :likes
  • has_many :tags
  • has_many :post_tag_relations

tagsテーブル

Column Type Options
name string null: false, uniqueness:: true

Association

  • has_many :posts
  • has_many :post_tag_relations

post_tag_relationテーブル

Column Type Options
tag references null: false, foreign_key: true
post references null: false, foreign_key: true

Association

  • belongs_to :post
  • belongs_to :tag

モデル作成

まずはモデルを作成します。
postモデルとtagモデルは多対多の関係になるので、中間テーブル(post_tag_relation)も同時に設ける必要があります。

# ターミナル
% rails g model tag
# ターミナル
% rails g model post_tag_relation

マイグレーションファイルを編集

20210206083851_create_tags.rb
class CreateTags < ActiveRecord::Migration[6.0]
  def change
    create_table :tags do |t|
      # 下記1行を追加
      t.string :name, null:false, uniqueness: true

      t.timestamps
    end
  end
end
20210206083924_create_post_tag_relations.rb
class CreatePostTagRelations < ActiveRecord::Migration[6.0]
  def change
    create_table :post_tag_relations do |t|
      # 下記2行を追加
      t.references :post, foreign_key: true
      t.references :tag, foreign_key: true

      t.timestamps
    end
  end
end

編集が終わったら、マイグレーションを行います

# ターミナル
% rails db:migrate

アソシエーションを記述

post.rb
class Post < ApplicationRecord
 # 省略

  has_many :post_tag_relations, dependent: :destroy
  has_many :tags, through: :post_tag_relations

 # 省略 
end
post_tag_relation.rb
class PostTagRelation < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end
tag.rb
class Tag < ApplicationRecord
  has_many :post_tag_relations, dependent: :destroy
  has_many :posts, through: :post_tag_relations

  validates :name, uniqueness: true
end

tag.rbでは同じタグが複数生成されないように、バリデーションを設けました。

Formオブジェクト導入

modelsディレクトリにposts_tag.rbファイルを作成します。
作成したら、1つのフォームから複数テーブルにデータを保存するためにファイルを編集していきます。

完成形は以下になります。(後から順番に説明します。)

posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id

  with_options presence: true do
    validates :images
    validates :title
    validates :name
    validates :category_id, numericality: { other_than: 1 , message: "は--以外から選んでください"} 
  end

  def save
    post = Post.create(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    PostTagRelation.create(post_id: post.id, tag_id: tag.id)
  end

  def update
    @post = Post.where(id: post_id)
    post = @post.update(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    map = PostTagRelation.where(post_id: post_id )
    map.update(post_id: post_id, tag_id: tag.id)
  end
end

順番に実装していくと、まずはPostsTagクラスを作り、include ActiveModel::Modelを追加します。

posts_tag.rb
class PostsTag
  include ActiveModel::Model
end

Formオブジェクトでは、クラスを自分で定義してモデルのように扱うことができ、ActiveModel::Modelというモジュールを読み込むことで、ヘルパーメソッドなどが使えます。


その後、1つのフォームで同時に保存したい値をattr_accessorを使ってPostsTagクラスで使えるようにします。

posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id
end

コントローラーに記述しているであろうparamsで送られてきたデータを渡すようなイメージです。

posts_controller.rb
#わかりやすくするために出してきただけなので、このタイミングで編集する必要はありません
private

  def post_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id)
  end

  def update_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id, post_id: params[:id])
  end

次に必要なバリデーションを記述します。
かなり人によって違うと思うので、必要なバリデーションを記述しましょう。

基本的には投稿機能を実装した際のバリデーションに、タグ名のバリデーションを足すような形になると思います。

posts_tag.rb
class PostsTag
  include ActiveModel::Model
  attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id

  with_options presence: true do
    validates :images
    validates :title
    validates :name
    validates :category_id, numericality: { other_than: 1 , message: "は--以外から選んでください"} 
  end

end

保存と編集をするためのメソッドを作ります。
やっていることとしては、順番に保存する記述をしているだけです。(編集は該当のレコードを探してくるコードが増えます。)
これを記述するとコントローラーで、PostsTag.saveやPostsTag.updateが使えるようになります。

posts_tag.rb
 def save
    # まずは投稿を保存
    post = Post.create(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    # その後タグを保存
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    # 最後に中間テーブルにpostとtagを紐付け、保存する
    PostTagRelation.create(post_id: post.id, tag_id: tag.id)
  end

  def update
    # 編集したい投稿をとってくる
    @post = Post.where(id: post_id)
    # データを更新
    post = @post.update(title: title, explanation: explanation, category_id: category_id, animal_name: animal_name, user_id: user_id, images: images)
    # その後タグを保存
    tag = Tag.where(name: name).first_or_initialize
    tag.save

    # 最後に更新したpostとtagを紐付け、中間テーブルを更新する
    map = PostTagRelation.where(post_id: post_id )
    map.update(post_id: post_id, tag_id: tag.id)
  end

formオブジェクト導入は以上です。

コントローラー編集

続いてコントローラーを編集します。

post_controller.rb

# 関係あるところだけ抜粋

before_action :find_post, only: [:show, :edit, :update, :destroy]

  def new
    @post = PostsTag.new
  end

  def create
    @post = PostsTag.new(post_params)
    if @post.valid?
      @post.save
      redirect_to root_path
    else
      render :new
    end
  end

  def edit
    @form = PostsTag.new(title: @post.title, animal_name: @post.animal_name, category_id: @post.category_id, 
      explanation: @post.explanation)
  end

  def update
    @form = PostsTag.new(update_params)
    if @form.valid?
      @form.update
      redirect_to root_path
    else
      render :edit
    end
  end

  private
  def post_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id)
  end

  def update_params
    params.require(:posts_tag).permit(:title, :explanation, :animal_name, :category_id, :name, images: []).merge(user_id: current_user.id, post_id: params[:id])
  end

  def find_post
    @post = Post.find(params[:id])
  end

ポイントは2つあるかなと思います

  • PostTagクラスのインスタンスに変更すること (例: Post.new → PostTag.new)
  • updateの際は、どの投稿かを判別する必要があるので、post_idもマージしてあげること

その他(該当する方だけ)

アクティブストレージで、画像投稿機能を実装している場合や、変数を@postから@formなどに変更している場合は、投稿フォームのビューのコードを修正する必要があります。

コントローラーで変数名を変えた場合はモデル名を合わせる必要があります。

# 変更なしの場合
<%= form_with model: @post, url: posts_path, local: true do |f| %>
<%= render "shared/error_messages", model: @post %>

# コントローラーで@formに変更した場合
<%= form_with model: @form, url: posts_path, local: true do |f| %>
<%= render "shared/error_messages", model: @form %>

アクティブストレージで、画像投稿機能を実装している場合はフォームのname属性を変更する必要があります。

# 変更前
<%= f.file_field(:images, name: 'post[images][]', class:"post-image", id:"post_image")%>

# 変更後
<%= f.file_field(:images, name: 'posts_tag[images][]', class:"post-image", id:"post_image")%>

また、エラーメッセージの日本語化を行っている場合は再び英語に戻っていると思うので、再度configファイルを編集する必要があります。

config/locales/models/ja.yml
# 変更前
ja:
  activerecord:
    models:
      post: 投稿
    attributes:
      post:
        images: ペットの写真
        title: 写真のタイトル
        category_id: ペットの種類
  time:
    formats:
      default: "%Y/%m/%d %H:%M"
config/locales/models/ja.yml
# 変更後
ja:
  activemodel:
    models:
      posts_tag: 投稿 
    attributes:
      posts_tag: 
        images: ペットの写真
        title: 写真のタイトル
        category_id: ペットの種類
        name: タグ
  time:
    formats:
      default: "%Y/%m/%d %H:%M"

ビューでタグを出力

あとはコントローラーとビューを編集し、タグを出力します。

posts_controller.rb
def show
   @tags = @post.tags
 end
show.html.erb
<tr>
  <th class="detail-item">タグ</th>
  <td class="detail-value">
     <% @tags.each do |tag| %>
       <span class="tags"><%= tag.name %></span>
     <% end %>
  </td>
</tr>

以上です。
参考になれば幸いです。

2
6
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
2
6