さらなる高みへ -higher ground -

プログラミング学習の定着を狙いとしたアウトプット(独り言)をしてみるブログ

ツイートにコメントできるようにする

ここではツイートに対してコメントができるようにする。
ツイートの投稿と似ているが、リソースのネストという考え方が出てくるので使いこなせるよう学習していく。

ツイートにコメントできるようにする

先ほどまではツイートの詳細画面を作ってきた。
次はこのツイート詳細画面からツイートに対してコメントができるようにする。

下記のようなものを作っていく。

【例】
https://tech-master.s3.amazonaws.com/uploads/curriculums//f0ca8dce1dc8fa38b8af9c4db06cda62.png

コメントはツイートとは別のテーブルで管理しなくてはならない
そのため、commentsテーブルを作る必要がある。
さらに、コメントはどのツイートに対してのコメントなのか、そして誰のコメントなのかが明示的でなくてはならない。

だとすると、userモデルとtweetモデルの2つとアソシエーションを組む必要がある。
色々と複雑そうに見えるが、テーブルを作り、ルーティング、コントローラ、ビューを作る流れはこれまでとほぼ同じである。

1点、ルーティングの仕方だけ少し違うので、そこだけ注意が必要である。

手順は以下のとおり。

作業内容

1.commentsテーブルを作る

2.ルーティングを記述する

3.フォームを作る

4.コントローラを作り、createアクションを記述する

5.ビューを編集する

1.commentsテーブルを作る

ツイートに対してのコメントをDBに保存するには、新しいテーブルとモデルを作らなければならない。
commentを保存するテーブルなのでcommentsテーブル、モデルはcommentモデルという名前になる。
テーブルの名前は複数形モデル名は単数形になっていることに注意する。

① commentモデルを作る

以下のコマンドをターミナルで実行し、commentモデルを作成する。

ターミナル

$ rails g model comment

 

新規作成されるファイル
  • app/models/comment.rb
  • db/migrate/201XXXXXXXXXXXX_create_comments.rb

commentモデルファイルとcommentsテーブルのマイグレーションファイル(=設計図)が作成された。

マイグレーションファイルを編集する

モデルを作った際にマイグレーションファイルができた。
次はこのマイグレーションファイルを編集して、commentsテーブルに必要なカラムの情報を追加していく。

先ほど作成したマイグレーションファイルを以下のように編集する。

db/migrate/201XXXXXXXXXXXX_create_comments.rb

class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.integer :user_id
      t.integer :tweet_id
      t.text :text
      t.timestamps
    end
  end
end

コメントの本文はtextカラムに保存していく。
ここで注意が必要なのは、カラムにuser_idとtweet_idがあることである。

コメントはまず誰が投稿したコメントなのか分かる必要があるため、結びつくユーザーのidを保存する必要がある。

コメントを投稿したユーザーのidを保存するカラムがuser_idとなる。
そしてもうひとつ、コメントはどのツイートに対してのコメントなのか明示する必要がある。

結びつくツイートのidを保存するカラムがtweet_idとなる。

③ マイグレートを実行する

マイグレーションファイルの記述は完成したので、次はrails db:migrateを実行してテーブルを作る。
以下のコマンドをターミナルで実行し、テーブルを作成する。

ターミナル

$ rails db:migrate

 

マイグレーションファイルが実行されたことにより、commentsテーブルができた。
phpMyAdminを見てテーブルが追加されたことを確認する。
https://tech-master.s3.amazonaws.com/uploads/curriculums//2b92e38ffb0d006a6a506eb51349ec08.png
④アソシエーションを定義する
先ほど、commentsテーブルのカラムにuser_idとtweet_idを作成した。
これによって、コメントに結びつくユーザーとツイートのidが保存できるカラムができた。
しかし実際にツイートに結びつくコメントを呼び出したり、コメントに結びつくツイートを呼び出したりするにはアソシエーションというものを定義しなければならない。
そのため、次はcommentsテーブル、tweetsテーブル、usersテーブルの3つのテーブルを結びつけるアソシエーションを記述する。
やり方はtweetsテーブルとusersテーブルを結びつけた時と同じである。
アソシエーションの図は少し複雑ですが、以下の通りである。
テーブルとテーブルをつなぐ棒線は一対多の関係を表している
例えばtweetsテーブルからusersテーブルに伸びる棒線は一人のユーザーが複数のツイートを持つことができる関係を表している。
【テーブル図】
https://tech-master.s3.amazonaws.com/uploads/curriculums//c37c98c8cfffbfde132534f4f43c76e2.png
では実際にアソシエーションを組んでいく。
以下のようにcomments、tweets、usersテーブルを編集してアソシエーションを記述する。
app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :tweet               #tweetsテーブルとのアソシエーション
  belongs_to :user                #usersテーブルとのアソシエーション
end

コメントは一人のユーザーと一つのツイートに従属するためbelongs_to :モデル単数形を使う。

app/models/tweet.rb

class Tweet < ApplicationRecord
  belongs_to :user
  has_many :comments               #commentsテーブルとのアソシエーション
end

ツイートは複数のコメントを持つことができるため、has_many :モデル複数形を使ってアソシエーションを組む。

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
  has_many :comments               #commentsテーブルとのアソシエーション
end

ユーザーは複数のコメントを投稿することができるため、has_many: モデル複数形を使ってアソシエーションを組む。

これで、3つのテーブルのアソシエーションを組むことができた。

2.ルーティングを記述する

commentsテーブルと、各モデルのアソシエーションは完成した。
次はコメントを投稿するためのルーティングを記述していく。
今回はルーティングを簡単に作成するためにresourcesメソッドを使って定義する。

resourcesメソッド

Railsの基本となるコントローラの7つのアクションで記載した7つのアクション名に対してのルーティングを自動で生成するメソッドである。

tweetsテーブルにとっての1ツイート、usersテーブルにとっての1ユーザー、つまり、テーブルに保存されるレコード1つの情報のことをリソースと呼ぶ。

以下の例のような記述では、bookというリソースに対して、基本となる7つのアクションをリクエストするルーティングが設定される。

【例】

config/routes.rb

 Rails.application.routes.draw do
  resources :books
end
ターミナルにて rake routes コマンドを実行すると以下のように7つのルーティングが設定されているのが確認できる。
【例】
ターミナル
 Prefix Verb         URI Pattern               Controller#Action
    books GET    /books(.:format)          books#index
          POST   /books(.:format)          books#create
 new_book GET    /books/new(.:format)      books#new
edit_book GET    /books/:id/edit(.:format) books#edit
     book GET    /books/:id(.:format)      books#show
          PATCH  /books/:id(.:format)      books#update
          PUT    /books/:id(.:format)      books#update
          DELETE /books/:id(.:format)      books#destroy

またcontrollerでbefore_actionの編集をした時と同様に、resourcesメソッドでもexceptやonlyオプションを組み合わせて特定のアクションへのルーティングのみを指定することができる。

コメントについてのルーティングを定義する前に、まずは今まで定義していたツイートとユーザーに関するルーティングをresourcesメソッドを使って書き換えて行く。

以下のようにルーティングをresourcesメソッドを使い書き換える。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root 'tweets#index'
  resources :tweets                     #tweets_controllerに対してのresourcesメソッド
  resources :users, only: [:show]       #users_controllerに対してのresourcesメソッド
end

tweetsコントローラは7つのアクション全て使っているが、usersコントローラはshowアクションしか使っていないため、onlyオプションでアクションの指定をする。

できたら、rake routesコマンドを使ってどのようにルーティングが定義されているか確認する。

今までのルーティングの記述とresourcesの関係

ここでは、今まで「get 'tweets' => 'tweets#index'」このような形式でルーティングを記述してきた。
わかりやすさを重視してこのように書いてきたが、resourcesを使用した方が少ないコードでタイプミスも起きにくいため、resourcesを使うことが一般的である。

以下のようにターミナルでrake routesコマンドを実行し、ルーティングを確認する。

ターミナル

$ rake routes

 

コマンドを実行して以下のようなルーティングが表示されれば成功。

ターミナル

Prefix     Verb   URI Pattern              Controller#Action
root       GET    /                            tweets#index
tweets     GET    /tweets(.:format)            tweets#index
           POST   /tweets(.:format)            tweets#create
new_tweet  GET    /tweets/new(.:format)        tweets#new
edit_tweet GET    /tweets/:id/edit(.:format)   tweets#edit
tweet      GET    /tweets/:id(.:format)        tweets#show
           PATCH  /tweets/:id(.:format)        tweets#update
           PUT    /tweets/:id(.:format)        tweets#update
           DELETE /tweets/:id(.:format)        tweets#destroy
(省略)

 

次はcommentsコントローラのルーティングを記述する。
今まで通りに記述すると、下記のようなルーティングになる。

【例】
config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root 'tweets#index'
  resources :tweets
  resources :comments, only: [:create]
  resources :users, only: [:show]
end

このルーティングによって発行されるパスは以下の通りである。

【例】
ターミナル

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

 

ここで考えてみたいのだが、コメントには必ずコメント先というものが存在している。
今回の場合だと、コメント先のツイートがなければどのツイートに対してのコメントなのか分からなくなってしまう。

しかし今のままではパスの中にどのツイートのコメントなのかということを示す情報がない。
コメントを投稿する際には、どのツイートに対するコメントなのかをパスから判断できるようにしたいため、ここではネストという方法を使っていく。

ルーティングのネスト

ネストとはネストは「入れ子構造」を表すプログラミング用語である。
入れ子構造とは、マトリョーシカのように何かの中に何かが入っている状態を言う。

https://tech-master.s3.amazonaws.com/uploads/curriculums//5dfc941656779f9b67b00ca28a395d51.png

ルーティングのネストとは

Railsのルーティングもネストさせることができる。
つまり、1つのルーティングの中に、別のルーティングを入れ子にすることができるのである。

ルーティングのネストは少し複雑な話なので、最初にネストが必要な理由から確認する。

PicTweetでは、tweetというデータとcommentsというデータが親子関係であるためルーティングのネストを使用している。

https://tech-master.s3.amazonaws.com/uploads/curriculums//07534e2e2e55358138db1cbfef8803a5.png

データが親子関係にあるということは、上図のように1つのつぶやき(親)に対して複数のコメント(子)が関連づけられていることを指している。
そのため、コメントを追加するときは、必ず「idが何番のtweet」に関するコメントなのかを特定しなくてはならない。

そのために、ルーティングのネストを利用する。ネストを使うと、コントローラーで親のidを取得できるようになる。
つまり、親子関係にあるデータのうち、「子」に当たるデータを操作したいときは、「親」と「子」のルーティングをネストさせる必要があるということになる。

では、コメントの新規作成機能を追加する中で、使い方を詳しく見ていく。

以下のようにルーティングをネストさせながら記述する。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root 'tweets#index'
  resources :tweets do
    resources :comments, only: [:create]
  end
  resources :users, only: [:show]
end

 

doendで挟むことで中の記述をネストさせることができる。
この記述によって、どのようなルーティングになるのだろうか?
ターミナルでrake routesコマンドを実行し、ルーティングを確認してみる。

以下のようにターミナルでrake routesコマンドを実行し、ルーティングを確認する。

ターミナル

$ rake routes

 

コマンドを実行して以下のようなルーティングが表示されれば成功。
ターミナル

Prefix Verb           URI Pattern                            Controller#Action
#中略
tweet_comments POST   /tweets/:tweet_id/comments(.:format)   comments#create
#中略

URLに注目する。
ネストによって、/tweets/:tweet_id/comments(.:format)となっている。
リンクを飛ばすときは、この:tweet_idのところにコメントと結びつくツイートのidを記述する。

そうするとparamsのなかにtweet_idというキーが追加され、コントローラで扱うことができる。
少し複雑であるため、実際にフォームやアクションを作成しながら順を追って説明していく。

3.フォームを作る

先ほどまではresourcesメソッドやネスト構造などを使いながらcommentsコントローラのcreateアクションを定義していた。
次はコメントを投稿するためのフォームを作成する。
コメントはツイートの詳細画面から投稿できると便利なのでviews/tweets/show.html.erbにフォームを記述する。

以下のようにshow.html.erbを編集し、コメントフォームを作成する。
tweets/show.html.erb

<div class="contents row">
  <div class="content_post" style="background-image: url(<%= @tweet.image %>);">
    <% if user_signed_in? && current_user.id == @tweet.user_id %>
      <div class="more">
        <span><%= image_tag 'arrow_top.png' %></span>
        <ul class="more_list">
          <li>
            <%= link_to '編集', "/tweets/#{@tweet.id}/edit", method: :get %>
          </li>
          <li>
            <%= link_to '削除', "/tweets/#{@tweet.id}", method: :delete %>
          </li>
        </ul>
      </div>
    <% end %>
    <%= simple_format(@tweet.text) %>
    <span class="name">
      <a href="/users/<%= @tweet.user.id %>">
        <span>投稿者</span><%= @tweet.user.nickname %>
      </a>
    </span>
  </div>
  <div class="container"> 
    <!-- ここからフォーム -->
    <% if current_user %>
      <%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %>
        <textarea name="text" placeholder="コメントする" rows="2" cols="30"></textarea>
        <input type="submit" value="SENT">
      <% end %>
    <% end %>
  </div>
</div>

25行目でif current_userとすることで、ログインしていない状態では投稿フォームを出さないようにしている。
これは、ログインしていない状態ではコメントを投稿できないようにするためである。

また、form_tagのurlのところに注目。
tweets/show.html.erb

    <%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %>

HTTPメソッドがpostメソッドになっている。これはツイートのフォームを作った際と同じである。
ここで重要なところはform_tagの後に続くかっこの中のurlである。
このurlの中に式展開させながら@tweet.idを記述することによって、paramsの中にコメントと結びつくツイートのidが追加される。

なぜこのようなことをする必要があるのか、その理由はコントローラを作るときに説明する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//be9bd0e6e984346977e7022dda354310.png

4.コントローラを作り、createアクションを記述する

次はコントローラとアクションを作成する。
まずはcommentsコントローラを作る必要がある。rails gコマンドでcommentsコントローラを作成する。

以下のコマンドをターミナルで実行し、commentsコントローラを作成する。

ターミナル

$ rails g controller comments
モデルと対応しているコントローラの名前は必ず複数形にする。
新規作成されるファイル
・app/controllers/comments_controller.rb
・app/views/comments
これでcommentsコントローラを作ることができた。
次はcommentsコントローラ内でcreateアクションを作る。createアクションは以下の手順で進めていく。
1.createアクションを定義する
2.ストロングパラメーターを指定する
3.リダイレクト先をtweets/show.htmlにする
① createアクションを定義する

comments_controller.rb

まずはcommentsコントローラにcreateアクションを定義する。

class CommentsController < ApplicationController
  def create
    Comment.create(text: params[:text], tweet_id: params[:tweet_id], user_id: current_user.id)
  end
end
クラスに対してcreateメソッドを使ってインスタンスを作成し、データベースに保存する。引数では、カラムと、カラムの値を指定している。
まずtextカラムにはparamsの中の"text"という情報を保存する。
user_idカラムにはcurrent_user.id(=ログインしているユーザーのid)を保存している。
次にtweet_idカラムだが、ネストしてurlに@tweet.id(ツイートのid)を記述してあるのでparams[:tweet_id]とするだけで簡単に情報が取ることができる。
② ストロングパラメーターを指定する
先程までのアクションの記述でコメントの保存はできる。
しかしストロングパラメーターを指定していないので、不正な情報を受け取ってしまう可能性がある。
そのため次はストロングパラメーターを指定して、必要なparamsだけを取ってこれるようにする。
以下のようにcommentsコントローラを編集する。
comments_controller.rb
class CommentsController < ApplicationController
  def create
    Comment.create(text: comment_params[:text], tweet_id: comment_params[:tweet_id], user_id: current_user.id)
  end

  private
  def comment_params
    params.permit(:text, :tweet_id)
  end
end
tweetsコントローラのcreateアクションを作った際とほぼ同じである。
privateメソッドを使って、comment_paramsメソッドをコントローラ以外のところで呼び出せないようにしていて、permitメソッドで受け取るparamsのキーを指定している。
③ リダイレクト先をtweets/show.html.erbにする
ルーティングを記述し、アクションを作成したら次に行うことはビューを作ることである。ここまで来て、次にすることはviews/commentsディレクトリ下にcreate.html.erbのファイルを作成することだと考えた方はとても素晴らしい。
しかし今回は新しくビューファイルを作るのではなく、既にあるツイートの詳細画面に戻る処理を記述する。
redirect_toメソッドを使って画面遷移の記述をしていく。
https://tech-master.s3.amazonaws.com/uploads/curriculums//e0f308abdc00d1cf14642c9ec765d306.png
以下のようにcommentsコントローラを編集する。
comments_controller.rb 
class CommentsController < ApplicationController
  def create
    @comment = Comment.create(text: comment_params[:text], tweet_id: comment_params[:tweet_id], user_id: current_user.id)
    redirect_to "/tweets/#{@comment.tweet.id}"   #コメントと結びつくツイートの詳細画面に遷移する
  end

  private
  def comment_params
    params.permit(:text, :tweet_id)
  end
end
 redirect_toの後にはルーティングのurlやprefixを記述することでそのアクションを実行することができた。
tweetsコントローラのshowアクションを実行するにはツイートのidが必要である。
ここではアソシエーションを使って、@commentと結びつくツイートのidを記述していく。

5.ビューを編集する

① コメントをビューに表示させる
コメントを投稿した後は、自分が投稿したコメントや他のユーザーのコメントが読めたりするようにしたい。
コメントを表示する場所はツイートの詳細画面とする。このようにするとコメントを投稿した後に、先ほど記述したredirect_toメソッドによってツイートの詳細画面に遷移してコメントの確認ができる。
しかしその前に、ツイートの詳細画面でツイートと結びつくコメントを表示するためには、その前のコントローラの段階で、コメントのレコードをデータベースから取得する必要がある。
なのでまずはtweetsコントローラのshowアクションを編集する。
以下のようにtweets_controller.rbを編集する。
tweets_controller.rb
class TweetsController < ApplicationController

# 中略

  def show
    @tweet = Tweet.find(params[:id])
    @comments = @tweet.comments.includes(:user)
  end

# 中略

end

表示させたいツイートのレコード@tweetは既に取得できているので、ツイートと結びつくコメントのレコードをデータベースから取得する。
tweetsテーブルとcommentsテーブルはアソシエーションが組まれているため、@tweet.commentsとすることで、@tweetについて投稿された全てのコメントのレコードを取得することができる。

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

以下のようにtweets/show.html.erbを編集する

コントローラで、あるツイートについて投稿された全てのコメントのレコードを取得することができたので、それをビューで表示する。

@commentsには複数のコメントのレコードが入っているので配列の形をとっている。
なのでビューに表示させるためにはeachメソッドを使って、ひとつひとつのレコードに分解してから表示させる。

さらに、コメントをしたユーザー名をクリックしたらそのユーザーページに遷移するようにしたいので、名前のところにlink_toメソッドを使ってリンクを作る(リンクはまだクリックしない)。

tweets/show.html.erb

# 中略

  <div class="container">
    <% if current_user %>
      <%= form_tag("/tweets/#{@tweet.id}/comments", method: :post) do %>
        <textarea name="text" placeholder="コメントする" rows="2" cols="30"></textarea>
        <input type="submit" value="SENT">
      <% end %>
    <% end %>
<!-- ここから追記してください -->
    <div class="comments">   
      <h4><コメント一覧></h4>
      <% if @comments %>
        <% @comments.each do |comment| %>
          <p>
            <strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %></strong>
            <%= comment.text %>
          </p>
        <% end %>
      <% end %>
    </div>
  </div>
</div>

こちらではif @commentsとすることで、もし@commentsが空だった場合でもエラーが起こらないようにしている。

eachメソッドで繰り返しコメントを表示します。名前のところはlink_toメソッドを使ってリンクを作る。
ユーザーのidはcomment.user_idと記述することで、コメントを投稿したユーザーのidをparamsとして送る。

16行目で「comment.user」と記述し、コメントしたユーザーのレコードを取得しているが、includesメソッドで既にデータベースからレコードを取得しているのでSQL文は発行されず、N + 1問題は発生しない。

cssファイルを編集する

コメントの一覧画面を綺麗に表示させるために、cssファイルを編集する。ここではrailsの実装の学習を目的としていますため、css等の説明は割愛する。

以下のようにassets/stylesheets/style.scssを編集する

style.scssの318行目以降のコードを全て削除して、以下のコードをコピー&ペーストをして編集する。
style.scss

  .success {
    @include boxBase(20px 0 ,30px);
    box-shadow: 0 0 10px rgba($black,0.2);
    background-color: $white;
    box-sizing: border-box;
    text-align: center;
    h3 {
      @include baseMargin;
    }
  }
}
form {
  h3 {
    @include baseMargin(0 0 20px);
    text-align: center;
    font-weight: normal;
    font-size: 20px;
    color: $dark;
  }
  input,textarea {
    width: 100%;
    @include boxBase(5px 0 15px,10px);
    border: 1px solid $gray;
    border-radius: 5px;
    font-family: "游ゴシック", "YuGothic";
  }
  input[type="submit"] {
    @extend .transition;
    background-color: $accent;
    border-radius: 20px;
    color: #fff;
    border: 0;
    font-size: 18px;
    &:hover {
      background-color: lighten($accent, 10%);
    }
  }
}
.container {
  @include boxBase(20px 0 ,30px);
  box-shadow: 0 0 10px rgba($black,0.2);
  background-color: $white;
  box-sizing: border-box;
}
.comments {
  padding: 5px;
  margin-top: 15px;
  a {
    color: $accent;
    &:hover {
      text-decoration: underline;
    }
  }
}

footer {
  @include boxBase;
  color: $gray;
  p {
    text-align: center;
  }
}
以下のようにtweets/new.html.erbを編集する

少しcssのクラスを編集したため、他のビューファイルも編集する必要がある。

以下のようにtweets/new.html.erbを編集する。
tweets/new.html.erb

<div class="contents row">
  <div class="container">
    <%= form_tag('/tweets', method: :post) do %>
      <h3>
        投稿する
      </h3>
      <input type="text" name="image" placeholder="Image Url">
      <textarea name="text" placeholder="text" rows="10" cols="30"></textarea>           
      <input type="submit" value="SENT">
    <% end %>
  </div>
</div>

③ マイページのビューを編集する

今、users_controller.rbは以下のようになっている。
users_controller.rb

class UsersController < ApplicationController
 def show
    @nickname = current_user.nickname
    @tweets = current_user.tweets.page(params[:page]).per(5).order("created_at DESC")
  end
end

ここユーザーページは自分自身のツイートしか見られない仕様になっていたので、current_userを使うことでログインしているユーザー、即ち自分自身のツイートを見ることが出来た。

しかし、今はツイートの詳細ページからコメントをしたユーザーのページに遷移出来るような仕様になっている。
そのためcurrent_userを使ったままでは、どのユーザー名をクリックしても自分自身のユーザーページしか表示されない(実際に試してみると、そのようになっているはずである)。

クリックしたユーザーのページに遷移できるように、users_controllerのshowアクション編集する。

users_controller.rb

class UsersController < ApplicationController
  def show
    user = User.find(params[:id])
    @nickname = user.nickname
    @tweets = user.tweets.page(params[:page]).per(5).order("created_at DESC")
  end
end

3行目では、コメント欄に表示されるユーザーをクリックすることで送られたidをparamsで取得して、そのユーザーのレコードをuserという変数に代入している。
5 行目ではアソシエーションを利用することで、そのユーザーが投稿したツイートを取得して、@tweetsに代入している。

要点チェック