今回は、既存のRailsアプリにコメント機能を追加しました。
- ポリモーフィック関連付け(Polymorphic Association)...異なる種類のモデル同士を関連づける方法の一つ。通常、Railsでは、あるモデルと別のモデルを関連付ける際に、外部キー(foreign key)を使用。ポリモーフィック関連付けを使用すると、1つのモデルが複数の他のモデルと関連付けることができる。1つのモデルに対して複数の他のモデルが関連付けられる場合に使用される。
関連付け概要
-
- ポリモーフィック関連性を実現するためには、関連するモデルを示すためのカラムが必要
モデルを設定
- コメントを投稿可能なモデルに
has_many
を追加し、コメントのモデルにbelongs_to
を追加することで、ポリモーフィック関連性を設定。
- コメントを投稿可能なモデルに
ビューを設定する
- コメントフォームを表示するビューに、投稿可能なモデルのIDとタイプを送信するフォームを追加する。
学んだこと✏️
1.Railsアプリにreports(日報)のCRUD(一覧、個別、作成、編集、削除)の機能を追加。
$ rails g scaffold report title:string content:text
2. migrationファイルを生成
$ rails g migration Addカラム名RefToテーブル名 カラム名:型名
$ rails g migration AddUserRefToReports user:references
上記のコマンドを実行すると、以下のようなmigrationファイルが生成される。
[timestamp]_add_user_ref_to_reports.rb
class AddUserRefToReports < ActiveRecord::Migration[6.0] def change add_reference :reports, :user, null: false, foreign_key: true end end
このmigrationファイルでは、reportsテーブルに対して、userテーブルに対する外部キーを追加するための処理を定義している。
3. $ rails db:migrate
を実行
これにより、新しいカラムがreportsテーブルに追加され、userテーブルとの関連が設定される。
4.Userモデルにhas_many :reportsの関連付けを追加
class User < ApplicationRecord has_many :reports, dependent: :destroy # 他のコード... end
5.Reportモデルにbelongs_to :userの関連付けを追加
class Report < ApplicationRecord belongs_to :user validates :user_id, presence: true validates :title, presence: true validates :content, presence: true # 他のコード... end
これで、UserとReportの関連付けが完了。関連付けを使用して、ユーザーによる日報の一覧を作成したり、ユーザーによる日報の作成を制限したりすることができる。
6.ログインしているユーザーが自分の日報のみ編集・削除できるようにする
ReportsController
で、以下のようにbefore_actionを設定。
class ReportsController < ApplicationController before_action :set_report, only: %i[show] before_action :check_user, only: %i[edit, update, destroy] # ... private def check_user @report = current_user.reports.find_by!(id: params[:id]) end
find_by!
メソッドは、指定された条件に合致するレコードが存在しない場合には、ActiveRecord::RecordNotFound
例外を発生させる。つまり、ログインしているユーザーに属する日報の中から、params[:id]
で指定されたIDを持つ日報が見つからない場合には、エラーが発生する。
以上のように実装することで、ログインしているユーザーが自分が投稿した日報のみ編集・削除できるようになる。
views/reports/show.html.erb
に以下を追加
<% if @report.user == current_user %> <%= link_to 'Edit', edit_report_path(@report) %> <%= link_to 'Destroy', @report, method: :delete, data: { confirm: 'Are you sure?' } %> <% end %>
編集・削除ボタンについて、ログインしているユーザーが、自分が投稿した日報の場合のみ表示されるようになる。 これにより、ログインしているユーザーは自分が投稿した日報のみ編集・削除ができ、他のユーザーの日報を編集・削除することができなくなる。
7.コメントモデルの作成(本題はここから)
BookモデルとReportモデルに従属するCommentモデルを作成する。
$ rails g model comment user:references commentable:references{polymorphic} content:text --no-test-framework
user:references
は、user_id
という名前の外部キーを作成するためのオプション。:references
は、生成するマイグレーションファイルで外部キー制約を作成することを指示する。これは、このコメントがどのユーザーによって投稿されたかを追跡するために使用される。commentable:references{polymorphic}
は、ポリモーフィック関連を設定するためのオプション。これは、コメントがどのモデルに対しても関連付けられる可能性があることを意味する。つまり、コメントがBook(本)、Report(日報)、または他の何かに対して投稿されることができる。content:text
は、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
belongs_to
は、Active Recordの関連付けを設定するためのメソッドで、この場合、CommentモデルがUserモデルと多対1の関係を持つことを示す。また、belongs_to
の第2引数にpolymorphic: true
を指定することで、Commentモデルが他のモデルとも多対1の関係を持つことを可能にする。validates
は、Active Recordのバリデーションを設定するためのメソッドで、この場合、Commentモデルのcontent属性が空でないことを要求している。 つまり、このコードは、Commentモデルを定義し、Userモデルとの多対1の関係、他のモデルとの多対1の関係、そしてcontent属性が空でないことを要求するバリデーションを設定している。
db/migrate/[timestamp]_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
は、多態性(ポリモーフィズム)を実現するためのカラム。commentable_type
は、コメントが関連するモデルのクラス名を文字列として格納。今回のケースだと、commentable_type
カラムのセットされるのは参照する親モデルのクラス名(Book
orReport
)。commentable_id
は、多態性を実現するためのカラムの一つで、コメントが関連するモデルのIDを格納。commentable_type
と一緒に使用することで、commentable_id
がどのモデルのIDかを識別することができる。 このように、commentable_type
とcommentable_id
を組み合わせることで、1つのコメントが複数のモデルに関連付けられる多態性の実現が可能になる。
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
as: :commentable
は、多態性を実現するためのオプションで、Commentモデルが他のモデルとも多対1の関係を持つことを可能にする。また、dependent: :destroy
は、BookまたはReportが削除された場合、関連するCommentも削除されるように設定。
8. コメントの作成
コメントの新規投稿や削除などを行うためのコントローラを作成。
$ 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: Comnent.model_name.human) else end end private def comment_params params.require(:comment).permit(:content) end end
- コメントオブジェクトの内容は、
comment_params
メソッドで許可されたパラメーターから取得。 ここでは、:content
パラメーターのみを許可。 私的なメソッドであるcomment_params
メソッドは、Strong Parametersを使用して、予期しないパラメーターがデータベースに保存されることを防止するために使用。
コメントのルーティングの設定
config/routes.rb
Rails.application.routes.draw do : resources :reports do resources :comments, only: :create, module: :reports end resources :books do resources :comments, only: :create, module: :books end : end
HTTP動詞 | パス | コントローラ#アクション | 名前付きルーティングヘルパー |
---|---|---|---|
POST | /books/:book_id/comments | books/comments#create | book_comments_path |
POST | /reports/:report_id/comments | reports/comments#create | report_comments_path |
これにより、上記URLでコメントの新規投稿と削除が可能になる。
Books::Commentsコントローラーの作成
$ rails generate controller Books::Comments --no-assets --no-helper --no-test-framework
app/controllers/books/comments_controller.rb
class Books::CommentsController < ApplicationController before_action :set_commentable private def set_commentable @commentable = Book.find(params[:book_id]) end end
Reports::Commentsコントローラーの作成
$ rails generate controller Reports::Comments --no-assets --no-helper --no-test-framework
app/controllers/reports/comments_controller.rb
class Reports::CommentsController < ApplicationController 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 %> <div class="field"> <%= f.label :content %> <%= f.text_area :content, required: true %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
- 2つのプレースホルダー(テンプレート内に置かれ、後で置換される変数のこと。同じメッセージの翻訳を複数の場所で再利用できる。)があり、それぞれ以下のように解釈
%{count}
- コメントに関連するエラーの数%{resource}
- コメントのクラス名を人間が読みやすい形式で表示したもの
例えば、コメントが保存できなかった場合には「1件のコメントを保存できませんでした」というように表示される。
form_with
はURLを生成するのにpolymorphic_pathを使用。polymorphic_path
は与えられたオブジェクトに対する正しいURLを自動的に生成するヘルパーメソッド。Railsはコントローラー名やアクション名を直接指定する必要がなく、オブジェクトに基づいて正しいURLを自動的に生成することができる。これにより、コードの再利用性が向上し、コードの保守性が高まる。
BookとReportの詳細画面からコメントを投稿
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
を生成
9.コメントの表示
コメント一覧パーシャルの作成
app/views/comments/_comments.html.erb
<div class="comments-container"> <strong></strong> <% if comments.any? %> <ul> <% @comments.each do |comment| %> <% if comment.persisted? %> <li> <%= comment.content %></p> <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?
- コメントが存在する場合にコメントを表示。コメントが存在しなければ
t('.no_comments')
。 - Rails API: any?
- コメントが存在する場合にコメントを表示。コメントが存在しなければ
if comment.persisted?
- ActiveRecordモデルで定義されたメソッド。コメントがDBに保存されていれば表示。
- Rails API: persisted?
link_to comment.user.name_or_email, comment.user
で、コメントの投稿者の名前またはメールアドレスをリンクにして表示。l comment.created_at, format: :short
mm/dd HH:MM
の形式で表示- 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 %>
10.コメント作成失敗時の処理
コメント作成失敗時に本または日報の詳細画面に遷移する
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.CommentsController render_commentable_show end end : end
- create アクションでは、コメント可能な対象(@commentable)に紐づくコメントを新規作成し、ログイン中のユーザーをコメントのユーザーとして設定。そして、コメントの保存に成功した場合は、@commentable にリダイレクトして成功メッセージを表示。コメントの保存に失敗した場合は、@commentable に関連するコメント一覧を取得し、コメント可能な対象の show ページをレンダリング。
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
Books::CommentsController
は、CommentsController を継承。render_commentable_show
メソッドは、Books::CommentsController
でオーバーライドされている。render_commentable_show
メソッドは、コメントが作成された書籍の show ページをレンダリングするために使用される。Books::CommentsController
では、before_action
メソッドでset_commentable
を実行し、コメントが作成される対象となる書籍を設定。
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
11.コメント削除・編集
ルーティングの設定
config/routes.rb
Rails.application.routes.draw do : resources :reports do resources :comments, only: %i[create destroy edit update], module: :reports end resources :books do resources :comments, only: %i[create destroy edit update], module: :books end end
コメントの投稿と削除の処理
comments_controller.rb
にコメントの新規投稿と削除の処理を実装。
HTTP動詞 | パス | コントローラ#アクション | 名前付きルーティングヘルパー |
---|---|---|---|
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 if @comment.user == current_user @comment.destroy redirect_to @commentable, notice: t('controllers.common.notice_destroy', name: Comment.model_name.human) else redirect_to @commentable, alert: t('controllers.common.alert_not_authorized') end end private def set_comment @comment = Comment.find(params[:id]) end : end
- コメントの削除について:
クライアントサイドのコード(JavaScriptやHTML)はユーザーのブラウザ上で実行されるため、ユーザーは開発者ツールを使用してこれらのコードを変更することができる。
- 例えば、開発者ツールを使ってブラウザのJavaScriptコンソールを開き、JavaScriptのコードを直接編集することで、動的なWebページの動作を変更することができる。 これにより、不正な操作を行うユーザーが、例えば本来許可されていない削除操作を実行したり、権限を超えたアクセスを行ったりする可能性がある。例えば、コメントを削除するページのURLを直接変更して他のユーザーのコメントを削除する、あるいは管理者権限のないユーザーが管理者用のページにアクセスする、といったような攻撃が考えられる。
上記のような不正な操作を防ぐために
- サーバーサイドでも適切な権限の制御を行う必要がある。例えば、コメントの削除処理を実行する前に、作成者のリクエストかどうかを確認する、ログインしているユーザーのみにアクセスを許可する、管理者権限を持つユーザーのみに特定の操作を許可する、などのサーバーサイドのロジックを実装することで、不正な操作を防止することができる。
- クライアントサイドでの制御はあくまで見た目や動作の改善などを目的とし、セキュリティ的な制御はサーバーサイドで行うべき。サーバーサイドでの適切な権限の制御は、システムのセキュリティを確保する上で重要な対策となる。
コメント削除・編集リンクの追加
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
を追記
app/views/comments/edit.html.erb
を作成
<h1><%= t('views.common.title_edit', name: Comment.model_name.human) %></h1> <%= render 'comments/form', commentable: @commentable, comment: @comment %> <%= link_to t('views.common.back'), @commentable %>