#コメント機能を実装しよう
ツイート詳細画面からツイートに対して、コメントが書き込めるようなコメント機能を実装しましょう。
コメントは、ツイートが必ず所有する情報ではないため、ツイートと別のテーブルで管理しなくてはなりません。
そのため、commentsテーブルを作る必要があります。
さらに、コメントはどのツイートに対してのコメントなのか、どのユーザーが投稿したコメントなのか判る必要があります。
そのため、userモデルとtweetモデルの2つにアソシエーションを組む必要があります。
まずはUserモデルとTweetモデルに加えて
コメントモデルを作成します。
% rails g model comment
マイグレーションファイルを編集しましょう。
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.integer :user_id
t.integer :tweet_id
t.text :text
t.timestamps
end
end
end
ここで注意する点は、カラムにuser_idとtweet_idがあることです。
「誰が投稿したコメントなのか」がわかる必要があるので、
関連するユーザーのidを保存する必要があります。
そのidを保存するカラム名をuser_idとしました。
加えて「どのツイートに対してのコメントなのか」が判る必要があります。
関連するツイートのidを保存するカラムは、tweet_idとしました。
##マイグレーションを実行しましょう
% rails db:migrate
# 「ctrl + C」でローカルサーバーを終了
# 再度、ローカルサーバーを起動
% rails s
Sequel Proを開き、テーブルが追加されているか確認しましょう。
ここで、3つのテーブル同士の関係性を確認しましょう。
テーブルとテーブルをつなぐ線は、1対多の関係を表しています。
例としてtweetsテーブルからcommentsテーブルに伸びる線は、
一つのツイートが複数のコメントを持つ様を表しています。
comment.rbを編集しましょう
コメントは、1人のユーザーと1つのツイートに所属するので、belongs_to :モデル単数形と記述することで、アソシエーションを定義します。
アソシエーションを定義しましょう。
###コメントモデルを編集
class Comment < ApplicationRecord
belongs_to :tweet # tweetsテーブルとのアソシエーション
belongs_to :user # usersテーブルとのアソシエーション
end
###ツイートモデルを編集
class Tweet < ApplicationRecord
validates :text, presence: true
belongs_to :user
has_many :comments, dependent: :destroy # commentsテーブルとのアソシエーション
end
dependent: :destroyオプションを追加記述することで、
親モデルのTweetモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除されます。
###ユーザーモデルを編集
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :tweets, dependent: :destroy
has_many :comments, dependent: :destroy # commentsテーブルとのアソシエーション
end
##コメントを投稿するためのルーティングを設定します。
コメントを投稿する際には、どのツイートに対するコメントなのかをパスから判断できるようにします。
ルーティングのネストという方法を使っていきます。
その一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るためです。
Rails.application.routes.draw do
resources :親となるコントローラー do
resources :子となるコントローラー
end
end
##ルーティングを設定しましょう
今回は「コメント情報を作る機能」を実装するのでcreateアクションのルーティングとします。
Rails.application.routes.draw do
devise_for :users
root to: 'tweets#index'
resources :tweets do
resources :comments, only: :create
end
resources :users, only: :show
end
ルーティングを確認してみましょう。
Prefix Verb URI Pattern Controller#Action
# 中略
tweet_comments POST /tweets/:tweet_id/comments(.:format) comments#create
# 中略
##コントローラーを作成しましょう。
% rails g controller comments
class CommentsController < ApplicationController
def create
Comment.create(comment_params)
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id])
end
end
user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
tweet_idカラムは、paramsで渡されるようにするので、params[:tweet_id]として保存しています。
続けて、コメントをしたらツイートの詳細画面に戻る処理をcomments_controller.rbに記述しましょう。redirect_toメソッドを使って記述します。
class CommentsController < ApplicationController
def create
comment = Comment.create(comment_params)
redirect_to "/tweets/#{comment.tweet.id}" # コメントと結びつくツイートの詳細画面に遷移する
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, tweet_id: params[:tweet_id])
end
end
tweetsコントローラーのshowアクションを実行するには、ツイートのidが必要です。
そのため、ストロングパラメーターを用いた上で変数commentに代入します。
次は、コメントを投稿するためのフォームを作ります。
コメントはツイートの詳細画面から投稿したいので、
views/tweets/show.html.erbにフォームを記述します。
<div class="contents row">
<div class="content_post" >
<p><%= @tweet.text %></p>
<p><%= image_tag @tweet.image.variant(resize: '500x500'), class: 'tweet-image' if @tweet.image.attached?%></p>
<span class="name">
<a href="/users/<%= @tweet.user.id %>">
<span>投稿者</span><%= @tweet.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @tweet.user_id %>
</span>
<%= link_to '編集', edit_tweet_path(@tweet.id), method: :get %>
<%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
<% end %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@tweet, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
</div>
</div>
ツイートの詳細画面でツイートと結びつくコメントを表示するためには、ビューを呼び出す前のコントローラーが実行されている時点で、まずはコメントのレコードをデータベースから取得する必要があるのでtweets_controller.rbのshowアクションを編集しましょう。
class TweetsController < ApplicationController
before_action :set_tweet, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@tweets = Tweet.includes(:user).order("created_at DESC")
end
def new
@tweet = Tweet.new
end
def create
Tweet.create(tweet_params)
end
def destroy
tweet = Tweet.find(params[:id])
tweet.destroy
end
def edit
end
def update
tweet = Tweet.find(params[:id])
tweet.update(tweet_params)
end
def show
@comment = Comment.new
@comments = @tweet.comments.includes(:user)
end
private
def tweet_params
params.require(:tweet).permit(:image, :text).merge(user_id: current_user.id)
end
def set_tweet
@tweet = Tweet.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
tweets/show.html.erbでform_withを使用して、comments#createを実行するリクエストを飛ばしたいので、@comment = Comment.newというインスタンス変数を生成しておかないといけません。
tweetsテーブルとcommentsテーブルはアソシエーションが組まれているので、@tweet.commentsとすることで、@tweetへ投稿されたすべてのコメントを取得できます。
ビューでは誰のコメントか明らかにするため、
アソシエーションを使ってユーザーのレコードを取得する処理を繰り返します。
その際「N+1問題」が発生してしまうので、includesメソッドを使って解決しています
コメントを表示するためにツイート詳細ページを編集しましょう。
<div class="contents row">
<div class="content_post" >
<p><%= @tweet.text %></p>
<p><%= image_tag @tweet.image.variant(resize: '500x500'), class: 'tweet-image' if @tweet.image.attached?%></p>
<span class="name">
<a href="/users/<%= @tweet.user.id %>">
<span>投稿者</span><%= @tweet.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @tweet.user_id %>
</span>
<%= link_to '編集', edit_tweet_path(@tweet.id), method: :get %>
<%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
<% end %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@tweet, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
<div class="comments">
<h4><コメント一覧></h4>
<% @comments.each do |comment| %>
<p>
<strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
<%= comment.text %>
</p>
<% end %>
</div>
</div>
</div>
以上で完成です。