【備忘録】【rails】コメント機能を実装する

#はじめに
掲示板サイトを作成中のプログラミング初学者です。
新規投稿機能が実装できたので、
お次にコメント機能を追加しました。

自分のための備忘録ですが、同じような機能を実装しようとしている方の参考になれば幸いです！

#前提
ユーザー認証にdeviseを使用しています。

#コメントモデルの生成

コメントは、投稿が必ず所有する情報ではないため、投稿と別のテーブルで管理しなくてはなりません。
そのため、commentsテーブルを作る必要があります。
さらに、コメントはどのツイートに対してのコメントなのか、どのユーザーが投稿したコメントなのか判る必要があります。

そのため、userモデルとpostモデルの2つにアソシエーションを組む必要があります。

```ruby:ターミナル
$ bundle exec rails g model Comment
```
これにより、
・app/modelsフォルダ下にコメントモデルファイルが生成（app/models/comment.rb）
・db/migrateフォルダ下にマイグレーションファイルが生成

下記のようになるように、マイグレーションファイルを編集する。

```ruby:マイグレーションファイル
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
```

その上で下記を実行。

```ruby:ターミナル
$ bundle exec rails db:migrate （データベースに変更を指示・反映する）
```

Sequel Proを開くか、もしくはMySQLを立ち上げて、テーブルが追加されているかどうか確認する。

```ruby:ターミナル
$ brew services start mysql@5.7
$ mysql --user=root --password　を実行。
```
データベースの中に入ったら下記を実行。

show tables from データベース名;

中にcommentsテーブルがあればOK！

# モデルにアソシエーションの記載

Commentモデルにはbelongs_toが自動で設定されていますが、
UserモデルとPostモデルには手動で追加する必要がある。



```ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end
```
ユーザーは複数のコメントを投稿できるので、has_many:複数形になります。



```ruby:app/models/post.rb
class Post < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end
```
投稿は複数のコメントを所有できるので、has_many:複数形になります。
dependent: :destroyオプションを追加記述することで、
親モデルのPostモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除されます。

```ruby:app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post  # postsテーブルとのアソシエーション
  belongs_to :user  # usersテーブルとのアソシエーション
end
```
コメントは一人のユーザーと一つの投稿に紐づいているので、belongs_to:モデル単数形になります。



# ルーティングの設定

コメントを投稿する際には、どの投稿に対するコメントなのかをパスから判断できるようにします。
ルーティングのネストという方法を使っていきます。
その一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るためです。


```ruby:【例】ルーティングのネスト
Rails.application.routes.draw do
  resources :親となるコントローラー do
    resources :子となるコントローラー
  end
end
```

今回は「コメント情報を作る機能」を実装するのでcreateアクションのルーティングとします。

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
  resources :posts do
    resources :comments, only: :create
  end
  resources :users, only: :show
end
```

ルーティングを確認してみましょう。

```ruby:ターミナル
$ bundle exec rails routes
```

```
Prefix Verb           URI Pattern                            Controller#Action
# 中略
post_comments POST   /posts/:post_id/comments(.:format)   comments#create
# 中略
```


# Commentsコントローラの作成

```ruby:ターミナル
$ bundle exec rails g controller comments
```


```ruby:app/controllers/comments_controller.rb
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, post_id: params[:post_id])
  end
end
```
user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
post_idカラムは、paramsで渡されるようにするので、params[:post_id]として保存しています。

続けて、コメントしたら投稿詳細画面に戻る処理をcomments_controller.rbに記述しましょう。redirect_toメソッドを使って記述します。


```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    comment = Comment.create(comment_params)
    redirect_to "/posts/#{comment.post.id}"  # コメントと結びつく投稿の詳細画面に遷移する
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
  end
end
```

postsコントローラーのshowアクションを実行するには、投稿のidが必要です。
そのため、ストロングパラメーターを用いた上で変数commentに代入します。

# Viewの編集

次は、コメントを投稿するためのフォームを作ります。
投稿の詳細画面からコメントしたいので、
views/posts/show.html.erbにフォームを記述します。

```ruby:app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @comment], local: true) do |form| %>
        <%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
        <%= form.submit "コメント" %>
      <% end %>
    <% else %>
      <strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
    <% end %>
  </div>
</div>
```

投稿の詳細画面で投稿と結びつくコメントを表示するためには、ビューを呼び出す前のコントローラーが実行されている時点で、まずはコメントのレコードをデータベースから取得する必要があるのでposts_controller.rbのshowアクションを編集しましょう。


```ruby:app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show]

  def index
    @posts = Post.includes(:user).order("created_at DESC")
  end

  def new
    @post = Post.new
  end

  def create
    Post.create(post_params)
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy
  end

  def edit
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
  end

  def show
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
  end

  private
  def post_params
    params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end
```

posts/show.html.erbでform_withを使用して、comments#createを実行するリクエストを飛ばしたいので、@comment = Comment.newというインスタンス変数を生成しておかないといけません。

postsテーブルとcommentsテーブルはアソシエーションが組まれているので、@post.commentsとすることで、@postへ投稿されたすべてのコメントを取得できます。

ビューでは誰のコメントか明らかにするため、
アソシエーションを使ってユーザーのレコードを取得する処理を繰り返します。
その際「N+1問題」が発生してしまうので、includesメソッドを使って解決しています。

コメントを表示するためにツイート詳細ページを編集しましょう。

```ruby:app/views/posts/show.html.erb
<div class="contents row">
    <div class="content_post" >
      <p><%= @post.text %></p>
       <p><%= image_tag @post.image.variant(resize: '500x500'), class: 'tweet-image' if @post.image.attached?%></p>
      <span class="name">
        <a href="/users/<%= @post.user.id %>">
        <span>投稿者</span><%= @post.user.nickname %>
      </a>
      <% if user_signed_in? && current_user.id == @post.user_id %>
      </span>
      <%= link_to '編集', edit_post_path(@post.id), method: :get %>
      <%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
       <% end %>
    </div>

    <div class="container">
    <% if user_signed_in? %>
      <%= form_with(model: [@post, @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>
```


以上で完成です。






▼参考記事▼

https://qiita.com/__kotaro_/items/8a6bda99dab61d2a72a5

https://murci.net/archives/2759

https://qiita.com/masakichi_eng/items/25b86200c2b01f14b5d0