さらなる高みへ -higher ground -

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

ツイートの表示を改善する2

前回の投稿に続き、ツイートの表示を改善する。
今回は、ツイートの中にユーザーのニックネームが表示されるようにしていく。

ツイート一覧画面にニックネームを表示する

現在、ツイートの一覧画面のツイートの右下には、投稿時に入力した「name」が表示されている。
その部分の表示を投稿者の「nickname」を表示するように実装する。

作業内容

1.投稿者名を表示するようにビューを変更する

2.ツイートからユーザー情報を先読みする

3.投稿画面のビューを変更する

4.投稿時のコントローラでの処理を変更する

5.tweetsテーブルから不要なカラムを削除する

1.投稿者名を表示するようにビューを変更する

ツイートの右下、tweetsテーブルのレコードの「name」カラムの値が表示されている部分に、ツイートの投稿者の「nickname」が表示されるようにビューの変更を行っていく。

「nickname」を表示する際にはアソシエーションを利用する。
Tweetモデルに対して「Tweet belongs to User」という形でアソシエーションを定義しているので、Tweetモデルのインスタンス.userと記述するだけでそのインスタンスが属しているUserモデルのインスタンスを取得することができる。
またコンソールを利用して例を見てみる。

【例】アソシエーションを利用しない

ターミナル

$ rails c
[1] pry(main)> tweet = Tweet.find(1)
[2] pry(main)> User.find(tweet.user_id)
=> #<;User id: 1, email: "test@gmail.com", encrypted_password: "@@@@@@@@@@@@@@@@@", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2014-12-06 09:00:00", last_sign_in_at: "2014-12-06 09:00:00", current_sign_in_ip:
"127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", nickname: "test_ruby">

usersテーブルに対して、idがtweetのuser_idと等しいレコードを取得していく。
これと同じことをアソシエーションを利用すると以下のように記述できる。

【例】アソシエーションを利用する

ターミナル

$ rails c
[1] pry(main)> tweet = Tweet.find(1)
[2] pry(main)> tweet.user
=> #<User id: 1, email: "test@gmail.com", encrypted_password: "@@@@@@@@@@@@@@@@@", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2014-12-06 09:00:00", last_sign_in_at: "2014-12-06 09:00:00", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", nickname: "test_ruby">

早速実装してみる。

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

app/views/tweets/index.html.erb

<div class="contents row" >
  <% @tweets.each do |tweet| %>
    <div class="content_post" style="background-image: url(<%= tweet.image %>);">
      <%= simple_format(tweet.text) %>
      <span class="name">
        <a href="">
          <span>投稿者</span><%= tweet.user.nickname %>
        </a>
      </span>
    </div>
  <% end %>
  <%= paginate(@tweets) %>
</div>
phpMyAdminでtweetsテーブルのuser_idの値を、全て現在ログインしているユーザーのid番号に変更する

https://tech-master.s3.amazonaws.com/uploads/curriculums//fbe91e02635db4691862595e443e6c18.gif

ビューを確認する前に、以下の注意事項を読む。

phpMyAdminで、tweetsテーブルにあるレコードのうちuser_idカラムが空のものに、現在存在しているusersテーブルのレコードのidを入力しているか確かめましょう。前の章でtweetを投稿した際にuser_idも一緒に保存するようにしましたが、phpMyAdminで確認すればわかる通り、それ以前に投稿したtweetはuser_idがnullのままになっているはずです。この時tweet.userはnilとなるため、tweet.user.nicknameと書くと、nullクラスに対してnicknameメソッドが実行されてしまいエラーとなります。

 トップページを開き、投稿一覧にnicknameが表示されているかを確認する

2.ツイートからユーザー情報を先読みする

上記の実装でツイートの一覧画面に投稿者の「nickname」が表示されるようになった。
しかし、ツイートからユーザーの情報を呼び出す際に「n+1問題」という問題が発生している。

そこで、今回はこの問題を解決するためにユーザー情報を先読みするように実装していく。

 

n+1問題

モデルを利用してデータベースの情報にアクセスする際にはSQLが発行される。
SQLが発行されるたびにデータベースに対して通信が走るため、SQLが大量に発行されれば処理が重くなる。

n+1問題とは、データを呼び出す際に大量のSQLが発行されてしまう問題のことである。(DBからデータを抽出する操作を、SQLを発行すると表現するのが一般的)

今回の場合、indexアクションで全ツイートを取得する1回に加えて、アソシエーションを利用してツイートの数だけユーザー情報を呼び出している。

つまり、現在の状態だとツイート数+1回SQLが発行されている。
この状態のことをn+1問題と言う。

includesメソッド

includesメソッドはn+1問題を解消することができる。

指定された関連モデルをまとめて取得することで、SQLの発行回数を減らすことができる。

書き方は、includes(:モデル名)とする。
引数で、関連モデルをシンボル型(接頭に:がつく型)で指定する。

今回、usersテーブルとtweetsテーブルの間には以下の図のような関係がある。
tweetsテーブルのレコードは必ず1つのusersテーブルのレコードに属しているため、includesメソッドを利用することでtweetsテーブルのレコードを取得する段階で関連するusersテーブルのレコードも一度に取得することができる。

app/controllers/tweets_controller.rbを以下のように編集する

app/controllers/tweets_controller.rb

class TweetsController < ApplicationController
  before_action :move_to_index, except: :index
  def index
    @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  end

  #以下省略

end

3.投稿画面のビューを変更する

ツイートを表示する際にアソシエーションを利用して投稿者のニックネームが表示されるようになったので、投稿時に「name」を入力する必要がなくなった。

そこで、この実装に合わせたビューの変更を行う。
投稿時に「name」を入力する必要がなくなったので、「name」の入力フォームを削除する。

app/views/tweets/new.html.erbを以下のように編集する。
6行目にあった以下の記述を削除する。

app/views/tweets/new.html.erb

<input type="text" name="name" placeholder="Nickname">

その結果、以下の内容になる。

app/views/tweets/new.html.erb

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

4.投稿時のコントローラでの処理を変更する

投稿時に「name」を入力する必要がなくなったため、それに合わせてtweetsコントローラでの処理も変更する。
nameカラムを使用しないため、ツイートの保存時にnameカラムに情報を保存しないように変更を行う。

app/controllers/tweets_controller.rbを以下のように編集する

app/controllers/tweets_controller.rb

class TweetsController < ApplicationController

  before_action :move_to_index, except: :index

  def index
    @tweets = Tweet.includes(:user).order("created_at DESC").page(params[:page]).per(5)
  end

  def new
  end

  def create
    Tweet.create(image: tweet_params[:image], text: tweet_params[:text], user_id: current_user.id)
  end

  private
  def tweet_params
    params.permit(:image, :text)
  end

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

 

5.tweetsテーブルから不要なカラムを削除する

ツイートに「name」という情報を保存しなくなったため「name」カラムが不要になった。
そこで、テーブルからカラムを削除するためのマイグレーションファイルを作成して、カラムの削除を実行する。
そのためには、以下のようにコマンドを実行します。

【例】
ターミナル

$ rails g migration Removeカラム名From削除元テーブル名 削除するカラム名:型

今回は、tweetsテーブルからstring型のnameというカラムを削除します。

ターミナルから、以下のコマンドを実行する

ターミナル

$ rails g migration RemoveNameFromTweets name:string
# マイグレーションファイルの作成

$ rails db:migrate
# マイグレーションの実行

Removeカラム名From削除元テーブル名カラム名の部分は、Addカラムの際と同様に必ずしも厳密なカラム名を入力する必要はない。
今回はnameというカラムのみを削除するので、Nameと記述する。
対して、削除元テーブル名は正確に記述する必要がある。

作成されたマイグレーションファイルは以下の内容になっている。

class RemoveNameFromTweets < ActiveRecord::Migration[5.2]
  def change
    remove_column :tweets, :name, :string
  end
end

tweetsテーブルを確認し、nameカラムが削除されているか確認する。

https://tech-master.s3.amazonaws.com/uploads/curriculums//b1beb497c2b05c6ba676ee19cccc4d48.gif

新規作成されるファイル

db/migrate/201XXXXXXXXXXX_remove_name_from_tweets.rb

usersコントローラーを編集する

マイページでもページネーションが使えるようpage, perメソッドを追加する。

app/controllers/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
 

 

また、tweetsテーブルのnameカラムを削除したため、users/show.html.erbの6行目でエラーが発生してしまっている。
6行目を削除する。

app/views/users/show.html.erb以下のように編集する

以下の記述を削除する。

app/views/users/show.html.erb

<span class="name"><%= tweet.name %></span>

削除後、以下の内容になる。

app/views/users/show.html.erb

<div class="contents row">
  <p><%= @nickname %>さんの投稿一覧</p>
  <% @tweets.each do |tweet| %>
    <div class="content_post" style="background-image: url(<%= tweet.image %>);">
      <%= simple_format(tweet.text) %>
    </div>
  <% end %>
  <%= paginate(@tweets) %>
</div>

要点チェック