つぶやきアプリを作ってみよう⑪

#コメント機能を実装しよう
ツイート詳細画面からツイートに対して、コメントが書き込めるようなコメント機能を実装しましょう。

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

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

まずはUserモデルとTweetモデルに加えて
コメントモデルを作成します。

```
% rails g model comment
```

マイグレーションファイルを編集しましょう。

```rb:db/migrate/XXXXXXXXXXXX_create_comments.rb
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テーブルに伸びる線は、
一つのツイートが複数のコメントを持つ様を表しています。

****ER図添付
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1008745/361d1740-f451-ff45-689a-27c592f602b6.png)


comment.rbを編集しましょう
コメントは、1人のユーザーと1つのツイートに所属するので、belongs_to :モデル単数形と記述することで、アソシエーションを定義します。

アソシエーションを定義しましょう。

###コメントモデルを編集

```rb:app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :tweet  # tweetsテーブルとのアソシエーション
  belongs_to :user  # usersテーブルとのアソシエーション
end
```

###ツイートモデルを編集

```rb:app/models/tweet.rb

class Tweet < ApplicationRecord
  validates :text, presence: true
  belongs_to :user
  has_many :comments, dependent: :destroy  # commentsテーブルとのアソシエーション
end
```

dependent: :destroyオプションを追加記述することで、
親モデルのTweetモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除されます。

###ユーザーモデルを編集

```rb:app/models/user.rb

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に追加してコントローラーに送るためです。

```rb:【例】ルーティングのネスト

Rails.application.routes.draw do
  resources :親となるコントローラー do
    resources :子となるコントローラー
  end
end
```

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

```rb:config/routes.rb

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
```

```rb: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, tweet_id: params[:tweet_id])
  end
end
```

user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
tweet_idカラムは、paramsで渡されるようにするので、params[:tweet_id]として保存しています。

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

```rb:app/controllers/comments_controller.rb
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にフォームを記述します。

```erb:app/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アクションを編集しましょう。

```app/controllers/tweets_controller.rb
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メソッドを使って解決しています

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

```erb:app/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 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>
```
以上で完成です。