開発環境
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
マイグレーションファイルを編集
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
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
アソシエーションを記述
class Post < ApplicationRecord
# 省略
has_many :post_tag_relations, dependent: :destroy
has_many :tags, through: :post_tag_relations
# 省略
end
class PostTagRelation < ApplicationRecord
belongs_to :post
belongs_to :tag
end
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つのフォームから複数テーブルにデータを保存するためにファイルを編集していきます。
完成形は以下になります。(後から順番に説明します。)
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を追加します。
class PostsTag
include ActiveModel::Model
end
Formオブジェクトでは、クラスを自分で定義してモデルのように扱うことができ、ActiveModel::Modelというモジュールを読み込むことで、ヘルパーメソッドなどが使えます。
その後、1つのフォームで同時に保存したい値をattr_accessorを使ってPostsTagクラスで使えるようにします。
class PostsTag
include ActiveModel::Model
attr_accessor :title, :explanation, :category_id, :animal_name, :name, :images, :user_id, :post_id
end
コントローラーに記述しているであろうparamsで送られてきたデータを渡すようなイメージです。
#わかりやすくするために出してきただけなので、このタイミングで編集する必要はありません
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
次に必要なバリデーションを記述します。
かなり人によって違うと思うので、必要なバリデーションを記述しましょう。
基本的には投稿機能を実装した際のバリデーションに、タグ名のバリデーションを足すような形になると思います。
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が使えるようになります。
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オブジェクト導入は以上です。
コントローラー編集
続いてコントローラーを編集します。
# 関係あるところだけ抜粋
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ファイルを編集する必要があります。
# 変更前
ja:
activerecord:
models:
post: 投稿
attributes:
post:
images: ペットの写真
title: 写真のタイトル
category_id: ペットの種類
time:
formats:
default: "%Y/%m/%d %H:%M"
# 変更後
ja:
activemodel:
models:
posts_tag: 投稿
attributes:
posts_tag:
images: ペットの写真
title: 写真のタイトル
category_id: ペットの種類
name: タグ
time:
formats:
default: "%Y/%m/%d %H:%M"
ビューでタグを出力
あとはコントローラーとビューを編集し、タグを出力します。
def show
@tags = @post.tags
end
<tr>
<th class="detail-item">タグ</th>
<td class="detail-value">
<% @tags.each do |tag| %>
<span class="tags"><%= tag.name %></span>
<% end %>
</td>
</tr>
以上です。
参考になれば幸いです。