[Rails]コメント 10/20
はじめに
投稿の詳細ページにコメント機能を入れていきます。
環境:
Rails 6.1.7.3
ruby 3.0.0
コメントモデルを作成する
bin/rails generate model Comment body:text article:references user:references
invoke active_record
create db/migrate/20230627133238_create_comments.rb
create app/models/comment.rb
class CreateComments < ActiveRecord::Migration[6.1]
def change
create_table :comments do |t|
t.text :body, null: false
t.references :article, :user, null: false, foreign_key: true
t.timestamps
end
end
end
bin/rails db:migrate
Running via Spring preloader in process 31692
== 20230627133238 CreateComments: migrating ===================================
-- create_table(:comments)
-> 0.0042s
== 20230627133238 CreateComments: migrated (0.0043s) ==========================
コメントモデルを編集する
アソシエーションとバリデーションを追加します。
class Comment < ApplicationRecord
belongs_to :article
belongs_to :user
validates :body. presence: true, length: { maximum: 65_535 }
end
class User < ApplicationRecord
...
has_many :articles, dependent: :destroy
has_many :comments, dependent: :destroy
end
class Article < ApplicationRecord
...
has_many :comments, dependent: :destroy
belongs_to :user
end
ネストしたリソースを作成する
今回必要なのはcreateアクションだけなので、%i[create]
とします。
resources :articles do
resources :comments, only: %i[create]
end
コントローラーを作成する
bin/rails generate controller Comments
create app/controllers/comments_controller.rb
invoke erb
create app/views/comments
invoke decorator
create app/decorators/comment_decorator.rb
コメントコントローラーにCRUDアクションを追加します。
class CommentsController < ApplicationController
def create
comment = Current.user.comments.build(comment_params)
if comment.save
flash[:success] = t('defaults.message.created', item: Comment.model_name.human)
redirect_to article_path(comment.article)
else
flash[:danger] = t('defaults.message.not_created', item: Comment.model_name.human)
redirect_back fallback_location: article_path(comment.article)
end
end
private
def comment_params
params.require(:comment).permit(:body).merge(article_id: params[:article_id])
end
end
# GOOD アソシエーション活用
comment = current_uesr.comments.build(comment_params)
# BAD 初期化した後に値を代入
comment = Comment.new(comment_params)
comment.user_id = current_user.id
# BAD 初期化する際のパラメータをmerge
comment = Comment.new(comment_params.merge(user_id: current_user.id))
article_comments POST /articles/:article_id/comments(.:format) comments#create
コメントを作成してみます。
irb(main):001:0> user = User.first
(0.8ms) SELECT sqlite_version(*)
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, user_name: "test_user", email: "user@sample.com", password_digest: [FILTERED], created_at: "2023-06-15 17:14...
irb(main):002:0> article = Article.first
Article Load (0.1ms) SELECT "articles".* FROM "articles" ORDER BY "articles"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Article id: 1, title: "hello", body: "rails", created_at: "2023-06-22 16:46:49.176551000 +0900", updated_at: "2023-06-22...
irb(main):003:0> comment = article.comments.build(user_id:1, body:"test comment")
=> #<Comment id: nil, body: "test comment", article_id: 1, user_id: 1, created_at: nil, updated_at: nil>
irb(main):004:0> comment.save
TRANSACTION (0.1ms) begin transaction
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Comment Create (0.9ms) INSERT INTO "comments" ("body", "article_id", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["body", "test comment"], ["article_id", 1], ["user_id", 1], ["created_at", "2023-06-27 16:02:19.999866"], ["updated_at", "2023-06-27 16:02:19.999866"]]
TRANSACTION (0.7ms) commit transaction
=> true
irb(main):005:0> comment = Comment.first
Comment Load (0.2ms) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Comment id: 1, body: "test comment", article_id: 1, user_id: 1, created_at: "2023-06-28 01:02:19.999866000 +0900", updat...
irb(main):004:0> comment = article.comments.build(body:"test comment")
=> #<Comment id: nil, body: "test comment", article_id: 1, user_id: nil, created_at: nil, updated_at: nil>
irb(main):005:0> comment.save
=> false
irb(main):006:0> comment.errors.full_messages
=> ["Userを入力してください"]
Articleコントローラーを編集する
show
アクションにコメントのインスタンス変数を渡します。
class ArticlesController < ApplicationController
def show
@article = Article.find_by(id: [params[:id]])
@comment = Comment.new
@comments = @article.comments.includes(:user).order(created_at: :desc)
if @article.nil?
flash[:danger] = "記事が見つかりませんでした。"
redirect_to root_path
end
end
end
N+1
問題を回避する
includesは、関連付けられたテーブルのデータを参照するメソッドです。このメソッドを使うと、あるモデルからデータを取得する際に、関連付けされたモデルのデータも一緒に取得してくれます。
# GOOD
@comments = @board.comments.includes(:user).order(created_at: :desc)
# BAD N+1問題が起こる書き方
@comments = @board.comment.order(created_at: :desc)
コメントフォームを作成する
コメントフォームをテンプレート化して投稿の詳細ページに読み込みます。
<%= form_with model: [article, comment] do |form| %>
<div class="mb-3">
<%= form.label :body %>
<%= form.text_area :body, class: 'form-control' %>
</div>
<div class="text-end">
<%= form.submit class: 'btn btn-primary btn-sm' %>
</div>
<% end %>
ローカル変数にインスタンス変数を渡します。
<%= render 'comments/form', { article: @article, comment: @comment } %>
コメント一覧ビューを作成する
コメント一覧もテンプレート化して投稿の詳細ページにコメントフォームの下に読み込みます。
コメントした人だけコメントの編集と削除を表示させます。
simple_format
はRailsのヘルパーメソッドです。
改行文字を含むテキストをブラウザ上で表示させるときに使います。
<% @article.comments.each do |comment| %>
<div class="card mb-4" id="comment-<%= comment.id %>">
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<p><%= simple_format(comment.body) %></p>
<% if comment.user_id == Current.user.id %>
<ul class='crud-menu-btn list-inline float-right'>
<li class="list-inline-item">
<%= link_to '#', id: "button-edit-#{comment.id}" do %>
<%= icon 'fa', 'pen' %>
<% end %>
</li>
<li class="list-inline-item">
<%= link_to '#', id: "button-delete-#{comment.id}" do %>
<%= icon 'fas', 'trash' %>
<% end %>
</li>
</ul>
<% end %>
</div>
<div class="d-flex justify-content-between">
<div class="d-flex flex-row align-items-center">
<%= image_tag 'profile.jpg',size:'25x25' %>
<p class="small mb-0 ms-2"><%= comment.user.user_name %></p>
</div>
<div class="d-flex flex-row align-items-center">
<p class="small text-muted mb-0"><%= l(comment.created_at, format: :long) %></p>
</div>
</div>
</div>
</div>
<% end %>
コメント一覧部分のパーシャルを作成
each文を使わずにテンプレートを繰り返し表示したいのでコメント一覧部分のパーシャルを作成します。
<%= render comments %>
ここではさらに_comment.html.erb
を呼び出しています。
<div class="card mb-4" id="comment-<%= comment.id %>">
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<p><%= comment.body %></p>
<% if Current.user.own?(comment) %>
<ul class='crud-menu-btn list-inline float-right'>
<li class="list-inline-item">
<%= link_to '#', id: "button-edit-#{comment.id}" do %>
<%= icon 'fa', 'pen' %>
<% end %>
</li>
<li class="list-inline-item">
<%= link_to '#', id: "button-delete-#{comment.id}" do %>
<%= icon 'fas', 'trash' %>
<% end %>
</li>
</ul>
<% end %>
</div>
<div class="d-flex justify-content-between">
<div class="d-flex flex-row align-items-center">
<%= image_tag 'profile.jpg',size:'25x25' %>
<p class="small mb-0 ms-2"><%= comment.user.user_name %></p>
</div>
<div class="d-flex flex-row align-items-center">
<p class="small text-muted mb-0"><%= l(comment.created_at, format: :long) %></p>
</div>
</div>
</div>
</div>
二重にパーシャル
ファイル名と複数形sを除いたオブジェクト名が一致しており、かつeach文のように繰り返し表示した場合は、このように省略することができます。
今回だと_comment.html.erb
とcomments
なので一致してます。
collection
オプションは、渡されたオブジェクトを、そのテンプレートを繰り返し表示することができるオプションです。
<%= render comments %>
<%= render partial: 'comment', collection: comments %>
<% comments.each do |comment| %>
<%= render partial: 'comment', locals: { comment: comment } %>
<% end %>
# コメントフォーム
<%= render 'comments/form', { article: @article, comment: @comment } %>
# コメント一覧
<%= render 'comments/comments', { comments: @comments } %>
比較用の汎用メソッドを作成する
投稿モデルとコメントモデルに共通の判定ロジックなので、Userモデルにまとめていきます。
ロジックをControllerやViewではなく、Modelに記載して呼び出すことで、メンテナンス時にModelだけの変更で住むようになるだけでなく、Fat Controllerを防ぐことにもなります。
class User < ApplicationRecord
def own?(object)
object.user_id == id
end
# 応用
def my_comment?(comment)
comment.user_id == id
# comment.user_id == self.id
end
end
ビューにインスタンスメソッドを書き換えます。
すっきりになりましたね。
# before
<% if Current.user && Current.user.id == article.user_id %>
<% if comment.user_id == Current.user.id %>
# after
<% if Current.user.own?(@article) %>
<% if Current.user.own?(comment) %>
localeファイルを編集する
コメント関連の翻訳を追加します。
ja:
activerecord:
models:
comment: 'コメント'
attributes:
comment:
body: 'コメント'
終わりに
モデルの関連付けが複雑になりましたのでMVCの仕組みを理解した上でどのような実装をすべきだったか改めて学び直しました。
Discussion