【Ruby on Rails】コメント機能を作ろう

#はじめに
学んだことを備忘録的に記事にしてます。間違い等あればアドバイスいただけると喜びます。

###作るもの
今回は投稿がある前提で投稿に対して自由にコメントできる機能を実装します。（題材は自分のポートフォリオサイトです。尚、すでにUserテーブルとMicropostテーブルは作成済みです。）

###対象読者
コメント機能を追加したい方。

#作成の流れ
コメント機能追加までの流れです。
1.Commentモデルの作成、変更
2.モデルの関連付け、バリデーションの設定
3.Commentsコントローラーの作成、ルーディングの設定
4.対応するビューの作成
５.コントローラのアクションの作成

#1.Commentsモデルの作成
まずデータベースとやりとりを行う、Commentモデルの作成を行います。

|カラム|型|
|:--|:--|
|id|integer|
|user_id|integer|
|micropost_id|integer|
|content|text|

誰がコメントしたかわかるようにuser_idはUserモデルと関連付けを行い、同じくmicropost_idもどのマイクロポストにコメントされたかわかるようにMicropostモデルと関連付けをこないます。
それではモデルを作成します。

```
rails g model comment user:references micropost:references content:text
```
```ruby:db/migrate/_create_comments.rb
class CreateComments < ActiveRecord::Migration[5.1]
  def change
    create_table :comments do |t|
      t.references :user, foreign_key: true
      t.references :micropost, foreign_key: true
      t.text :content, null: false

      t.timestamps
    end
  end
end
```
モデルを作成する際にreferencesとすることでindexと外部キー制約（foregin_key: true）が自動で追加されます。indexをuser_idとmicropost_idに追加することによってそれぞれに関連したコメントを探す際にデータを高速に調べられるようになります。また、外部キー制約がつくことによってuser_id（microposｔ_id）にはUserテーブルに存在するidのみデータベースレベルで保存するようになります。
また、コメントが空だとコメント機能の意味をなさないためnull:falseを追加します。
それではテーブルを作成します。

```
rails:db:migrate
```
#2.モデルの関連付け、バリデーションの設定
作成されたモデルはこんな感じです。


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

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :content, presence: true, length: { maximum: 140 }
end
```
自動でbelongs_toで一対一の関連付けができています。User,Micropostモデルにはそれぞれ手動で追加する必要があるので追加していきます。また、バリテーションも追加しています。テーブル作成の時点でcontentにはnull:falseで空で保存させないようにしていますが空文字（""）は保存できます。なのでpresence:trueを追加することによって空文字も拒否するようになります。また、バリデーションに基づいたエラーメッセージも保存されます。

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :comments, dependent: :destroy
```

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
```

UserもMicropostも多数のコメントを持てるためhas_manyで1対多の関連付けを行います。dependent: :destroyでUser、Micropostが消えた際に関連するコメントも消えるようにしています。

#3Comenntsコントローラーの作成、ルーディングの設定
続いてコントローラーを作成します。今回はcreateとdestroyアクションのみ使用します。

```
rails g controller comments
```

```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create 
  end
  
  def destroy
  end
end
```

ルーディングの設定。

```ruby:config/routes.rb
resources :microposts, only: [:new, :show, :create, :destroy] do
   resources :comments, only: [:create, :destroy]
end
```
commetsはmicropostsとネストして親子の関係を持たせます。こうすることによってコメントを作成する際に関連しているmicropostのidを取得することが容易になります。ネスト有無の違いはこんな感じです。commentsがmicropstsの下の階層についているのがわかります。

ネスト有

```
micropost_comments POST   /microposts/:micropost_id/comments(.:format) comments#create
micropost_comment DELETE /microposts/:micropost_id/comments/:id(.:format) comments#destroy
```
ネスト無し

```
comments POST   /comments(.:format) comments#create
comment DELETE /comments/:id(.:format) comments#destroy
```

#4対応するビューの作成
今回はMicropostの詳細ページからコメントするような形をとります。
コメントを表示するのはMicropost_show(Micropostｓ/:id)になるのでまずはコントローラーにコメント表示とコメントフォーム用にインスタンス変数を作成しておきます。

```ruby:app/controllers/microposts_controller.rb
def show
    @comment =Comment.new #新規コメント用
    @micropost = Micropost.find(params[:id])
    @comments =micorpost.comments #コメント表示用投稿に関連づくコメントの取得
end
```
コメント表示（投稿者と中身のみ）

```ruby:app/views/microposts/show.html.erb
<% @comments.each do |comment| %>
<%= comment.user.name %>
<%= comment.content %>
<% end %>
```

コメントフォーム

```ruby:app/views/microposts/show.html.erb
<%= form_with model: [@micropost, @comment], local: true do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
    
    <div class = 'form-group'>
      <%= f.text_area :content, class: 'form-control',　id:'content'\
         placeholder: "コメントを記入してください" %>
    </div>
   
    <%= f.submit "コメントする", class: "btn btn-primary" %>
<% end %>
```
コメントはいずれかのマイクロポストと関連づいているため、どのマイクロポストのコメントなのかという情報が必要になります。
そのため、form_withにmicropostのidも渡します。@comment=Comment.newで新規に値を作成しているので作成ボタンを押すと自動的にcreateアクションが動きます。


#５.コントローラのアクションの作成
次はコメントを作成するcreateアクションを作っていきます。

```ruby:app/contorollers/comments_contoroller.rb
class CommentsController < ApplicationController
  before_action :logged_in_user, only: :create

  def create 
    @comment = current_user.comments.build(comment_params)
    @comment.micropost_id = params[:micropost_id]
    if @comment.save
      flash[:success] = 'コメントしました'
      redirect_to @comment.micropost
    else
      @micropost = Micropost.find(params[:micropost_id])  
      @comments = @micropost.comments
      render template: 'microposts/show'
    end

 private
  
  def comment_params
    params.require(:comment).permit(:content)
  end
end
```

createアクション失敗後に再度同じページを表示するため、コメントを取得してます。（このやり方があっているかはわかりません。）コメントフォーム専用のページを設ければこの取得はいらないです。

コメントを作成する際に、ユーザーのidを渡す必要がありますが、current_user.comments.buildとすることによりログイン中ユーザーのidを入れ込みます。このままだとマイクロポストのiｄがなくコメントを作成できないのでform_withから送られてくるmicropost_idを取得しています。
（ストロングパラメータでmicoropst_idを取得する方法がわかりませんでした。）

これでコメントフォームからコメントをすることができるようになりました。
続いてコメント削除です。
まずはviewsに削除リンクを埋め込みます。

```ruby:app/views/microposts/show.html.erb

<% @comments.each do |comment| %>
  <%= comment.user.name %>
  <%= comment.content %>
<%= link_to '削除', micropost_comment_path(@micropost, comment), method: :delete %>
<% end %>
```
destroyアクションを動かすためmethod: :deleteを指定します。
paｔｈにはコメントがいずれかのマイクロポストに紐づいている関係上(@micropost,comment)を渡す必要があります。

```
micropost_comment DELETE /microposts/:micropost_id/comments/:id(.:format) comments#destroy
```


```ruby:app/contorollers/comments_contoroller.rb
class CommentsController < ApplicationController
  before_action :correct_user,   only: :destroy
  def destroy
    @comment = Comment.find(params[:id])
    @comment.destroy
    flash[:success] = 'コメントを削除しました'
    redirect_to @comment.micropost
  end

 private
  def correct_user
    @comment = current_user.comments.find_by(id: params[:id])
    redirect_to root_url if @comment.nil?
  end
end
```

#終わりに

間違いや、認識違い等があればコメントください。
それではここまで読んんでいただいきありがとうございました。