この記事では、Railsのポリモーフィック関連付けを使ってコメント投稿機能を実装する際のポイントを紹介します。
1. バージョン情報
- macOS:12.6
- Ruby:3.1.2
- Rails:6.1.6
2. 前提条件
- Userモデル(ユーザー)
- Bookモデル(本)のCRUD
- Userモデルに紐づいたReportモデル(日報)のCRUD
3. 実装時のポイント
3-1. ポリモーフィック関連付け
BookモデルとReportモデルに従属するCommentモデルの作成
$ rails g model comment user:references commentable:references{polymorphic} content:text --no-test-framework
app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :commentable, polymorphic: true
validates :content, presence: true # 追記
end
db/migrate/XXXX_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.1]
def change
create_table :comments do |t|
t.references :user, null: false, foreign_key: true
t.references :commentable, polymorphic: true, null: false
t.text :content
t.timestamps
end
end
end
マイグレーションを実行
$ rails db:migrate
db/schema.rb
create_table "comments", force: :cascade do |t|
t.integer "user_id", null: false
t.string "commentable_type", null: false
t.integer "commentable_id", null: false
t.text "content"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable"
t.index ["user_id"], name: "index_comments_on_user_id"
end
commentable_typeカラムにセットされるのは参照する親モデルのクラス名(BookまたはReport)
app/models/book.rb
class Book < ApplicationRecord
has_many :comments, as: :commentable, dependent: :destroy # 追記
end
app/models/report.rb
class Report < ApplicationRecord
belongs_to :user
has_many :comments, as: :commentable, dependent: :destroy # 追記
validates :title, presence: true
validates :content, presence: true
end
3-2. コメント作成
ルーティングの設定
config/routes.rb
Rails.application.routes.draw do
# 省略
resources :books do
resources :comments, only: :create, module: :books
end
resources :reports do
resources :comments, only: :create, module: :reports
end
end
module: :booksとmodule: :reports
| HTTP verb | Path | Controller#Action | Helper |
|---|---|---|---|
| POST | /books/:book_id/comments | books/comments#create | book_comments_path |
| POST | /reports/:report_id/comments | reports/comments#create | report_comments_path |
Commentsコントローラーの作成
$ rails g controller Comments --no-assets --no-helper --no-test-framework
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @commentable.comments.build(comment_params)
@comment.user = current_user
if @comment.save
redirect_to @commentable, notice: t('controllers.common.notice_create', name: Comment.model_name.human)
else
# コメント作成失敗時の処理は後述
end
end
private
def comment_params
params.require(:comment).permit(:content)
end
end
Books::Commentsコントローラーの作成
app/controllers/books/comments_controller.rb
class Books::CommentsController < CommentsController
before_action :set_commentable
private
def set_commentable
@commentable = Book.find(params[:book_id])
end
end
Reports::Commentsコントローラーの作成
app/controllers/reports/comments_controller.rb
class Reports::CommentsController < CommentsController
before_action :set_commentable
private
def set_commentable
@commentable = Report.find(params[:report_id])
end
end
コメント投稿フォームの作成
app/views/comments/_form.html.erb
<%= form_with model: [commentable, comment] do |f| %>
<% if comment.errors.any? %>
<div id="error_explanation">
<h2>
<%= I18n.t("errors.messages.not_saved",
count: comment.errors.count,
resource: comment.class.model_name.human.downcase) %>
</h2>
<ul>
<% comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.text_field :content, required: true %>
<%= f.submit t('.create') %>
<% end %>
form_with model: [commentable, comment]- form_withはURLを生成するのにpolymorphic_pathを使っている
本と日報の詳細画面からコメントを投稿できるようにする
app/controllers/books_controller.rb
class BooksController < ApplicationController
# 省略
def show
@comment = Comment.new
end
end
- フォームに渡す
@commentを定義
app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 省略
def show
@comment = Comment.new
end
end
- フォームに渡す
@commentを定義
app/views/books/show.html.erb
<%= render 'comments/form', commentable: @book, comment: @comment %>
- フォーム側で
book_comments_pathを生成
app/views/reports/show.html.erb
<%= render 'comments/form', commentable: @report, comment: @comment %>
- フォーム側で
report_comments_pathを生成
i18nで日本語化
ja.yml
ja:
activerecord:
models:
comment: コメント
controllers:
common:
notice_create: "%{name}が作成されました。"
comments:
form:
create: コメントする
3-3. コメント表示
コメント一覧パーシャルの作成
app/views/comments/_comments.html.erb
<div class="comments-container">
<strong><%= Comment.model_name.human %>:</strong>
<% if comments.any? %>
<ul>
<% comments.each do |comment| %>
<% if comment.persisted? %>
<li>
<%= comment.content %>
<small>
(<%= link_to comment.user.name_or_email, comment.user %> - <%= l comment.created_at, format: :short %>)
</small>
</li>
<% end %>
<% end %>
</ul>
<% else %>
(<%= t('.no_comments') %>)
<% end %>
</div>
if comments.any?- コメントが1件もなければ
t('.no_comments') - Rails API: any?
- コメントが1件もなければ
if comment.persisted?- コメントがDBに保存されていれば表示
- Rails API: persisted?
l comment.created_at, format: :short10/24 11:32の形式で表示- Railsガイド: Rails 国際化(i18n)API - 3.4 日付・時刻フォーマットを追加する
app/models/user.rb
class User < ApplicationRecord
# 省略
def name_or_email
name.empty? ? email : name
end
end
name_or_email- ユーザー名が入力されていればユーザー名を表示、未入力ならメールアドレスを表示
app/controllers/books_controller.rb
class BooksController < ApplicationController
# 省略
def show
@comment = Comment.new
@comments = @book.comments # 追記
end
end
- コメント一覧パーシャルに渡す
@commentsを定義
app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 省略
def show
@comment = Comment.new
@comments = @report.comments # 追記
end
end
- コメント一覧パーシャルに渡す
@commentsを定義
app/views/books/show.html.erb
<%= render 'comments/comments', comments: @comments %>
app/views/reports/show.html.erb
<%= render 'comments/comments', comments: @comments %>
i18nで日本語化
ja.yml
ja:
activerecord:
models:
comment: コメント
comments:
comments:
no_comments: コメントはまだありません
3-4. コメント作成失敗時の処理
コメント作成失敗時に本または日報の詳細画面に遷移する
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
@comment = @commentable.comments.build(comment_params)
@comment.user = current_user
if @comment.save
redirect_to @commentable, notice: t('controllers.common.notice_create', name: Comment.model_name.human)
else
# コメント作成失敗時の処理を追記
@comments = @commentable.comments
render_commentable_show
end
end
# 省略
end
app/controllers/books/comments_controller.rb
class Books::CommentsController < CommentsController
before_action :set_commentable
private
# 省略
def render_commentable_show
@book = @commentable
render 'books/show'
end
end
app/controllers/reports/comments_controller.rb
class Reports::CommentsController < CommentsController
before_action :set_commentable
private
# 省略
def render_commentable_show
@report = @commentable
render 'reports/show'
end
end
i18nで日本語化
ja.yml
ja:
activerecord:
models:
comment: コメント
controllers:
common:
notice_create: "%{name}が作成されました。"
3-5. コメント削除・編集
ルーティングの設定
config/routes.rb
Rails.application.routes.draw do
# 省略
resources :books do
resources :comments, only: %i[create destroy edit update], module: :books
end
resources :reports do
resources :comments, only: %i[create destroy edit update], module: :reports
end
end
| HTTP verb | Path | Controller#Action | Helper |
|---|---|---|---|
| POST | /books/:book_id/comments | books/comments#create | book_comments_path |
| GET | /books/:book_id/comments/:id/edit | books/comments#edit | edit_book_comment_path |
| PATCH | /books/:book_id/comments/:id | books/comments#update | book_comment_path |
| DELETE | /books/:book_id/comments/:id | books/comments#destroy | book_comment_path |
edit・update・destroyアクションの追加
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :set_comment, only: %i[edit update destroy]
def edit; end
def create
# 省略
end
def update
if @comment.update(comment_params)
redirect_to @commentable, notice: t('controllers.common.notice_update', name: Comment.model_name.human)
else
render :edit
end
end
def destroy
@comment.destroy
redirect_to @commentable, notice: t('controllers.common.notice_destroy', name: Comment.model_name.human)
end
private
def set_comment
@comment = Comment.find(params[:id])
end
# 省略
end
コメント削除・編集リンクの追加
app/views/comments/_comments.html.erb
<div class="comments-container">
<strong><%= Comment.model_name.human %>:</strong>
<% if comments.any? %>
<ul>
<% comments.each do |comment| %>
<% if comment.persisted? %>
<li>
<%= comment.content %>
<small>
(<%= link_to comment.user.name_or_email, comment.user %> - <%= l comment.created_at, format: :short %>)
</small>
<% if current_user == comment.user %>
<%= link_to t('views.common.edit'), edit_polymorphic_path([commentable, comment]) %> |
<%= link_to t('views.common.destroy'), polymorphic_path([commentable, comment]), method: :delete, data: { confirm: t('views.common.delete_confirm') } %>
<% end %>
</li>
<% end %>
<% end %>
</ul>
<% else %>
(<%= t('.no_comments') %>)
<% end %>
</div>
edit_polymorphic_path([commentable, comment])"/books/1/comments/1/edit"または"/reports/1/comments/1/edit"
polymorphic_path([commentable, comment])"/books/1/comments/1"または"/reports/1/comments/1"
app/views/books/show.html.erb
<%= render 'comments/comments', commentable: @book, comments: @comments %>
commentable: @bookを追記
app/views/reports/show.html.erb
<%= render 'comments/comments', commentable: @report, comments: @comments %>
commentable: @reportを追記
i18nで日本語化
ja.yml
ja:
activerecord:
models:
comment: コメント
views:
common:
edit: 編集
destroy: 削除
delete_confirm: よろしいですか?
controllers:
common:
notice_update: "%{name}が更新されました。"
notice_destroy: "%{name}が削除されました。"
comments:
comments:
no_comments: コメントはまだありません