hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

Pictweetの作成 その2

7:ツイートにユーザー情報を追加

現在ユーザーが投稿したツイートはどのユーザーが投稿したものなのかわからない状態になっている。これがわかるようになると、特定のユーザーがつぶやいたツイート一覧を取得できたり、ツイートの削除を行う際につぶやいた本人しか削除できないように設定できたりと色々便利。そこで、ツイートにユーザーの情報を追加して、誰が投稿したツイートなのかが分かるようにする。

 作業内容

1.tweetsテーブルにカラムを追加する
2.ツイート保存時にユーザー情報を追加する処理を記述する
3.ツイート保存時にユーザー情報も保存できたか確かめる

1.tweetsテーブルにカラムを追加する

ツイートにユーザーの情報を追加する場合、tweetsテーブルに新しくuser_idというカラムを作成し、そこにツイートを投稿したユーザーのidを入れる。カラムの追加の手順はnicknameカラムを追加した際と同様。

ターミナル
1
2
3
4
5
 $ rails g migration AddUserIdToTweets user_id:integer
 # tweetsテーブルにuser_idカラムをinteger型で追加するマイグレーションファイルの作成

 $ rake db:migrate
 # マイグレーションファイルの実行

 新規作成されるファイル

  • db/migrate/2014XXXXXXXXXX_add_user_id_to_tweets.rb

2.ツイート保存時にユーザー情報を追加する処理を記述

上記の作業でtweetsテーブルにカラムが追加されたので、ツイートの保存時に投稿したユーザーのidをテーブルに保存する処理を記述する。

 current_user

deviseでログイン機能を実装すると、current_userというヘルパーメソッドを使用することができる。これは、現在ログイン中のユーザーのレコードを、userクラスのインスタンスとして取得することができるメソッド。

usersテーブルでのidカラムの値が1のアカウントでログインしている場合、current_userUser.find(1)と同じ意味を持つ。この時、以下のようにcurrent_user.usersテーブルのカラム名とすることで、ログイン中のユーザーの情報として登録されている各カラムの値を取得することができる。

【例】
1
2
3
4
5
6
# current_userを利用して、ログインしているユーザーのデータを取得する
current_user.name
=> "takashi"

current_user.email
=> "hoge@gmail.com"

ツイートを投稿したユーザーとはつまり現在ログインしているユーザーのこと。なので、tweetsテーブルのuser_idカラムに保存すべきなのはcurrent_userのidカラムの値

実際にツイートをテーブルに保存する処理が書かれているのはtweets_controllercreateアクション。現在は、以下のようになっている。

app/controllers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  class TweetsController < ApplicationController

    before_action :move_to_index, except: :index

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

    def new
    end

    def create
      Tweet.create(tweet_params)
    end

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

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

ツイートを保存する際、nameimagetextというビューから送られてくる情報に加えて、user_idカラムにログイン中のユーザーのidを保存しなければならない。そこで、createメソッドの引数をcreate(カラム名: 保存する値, カラム名: 保存する値, …)と変更する必要がある。この時、name、image、textを保存する際にはtweet_paramsというストロングパラメーターを使用する。tweet_paramsはハッシュの形なので、tweet_params[:キーの名前]とすることでそのキーのバリューの値を取ることができる。すると、以下のように書くことができる。

app/controllers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  class TweetsController < ApplicationController

    before_action :move_to_index, except: :index

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

    def new
    end

    def create
      Tweet.create(name: tweet_params[:name], image: tweet_params[:image], text: tweet_params[:text])
    end

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

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

加えて、user_idに現在ログイン中のユーザーのidを保存する処理を追記。

 問題7:ツイートの保存時に、user_idに現在ログイン中のユーザーのidを保存する

作業ファイル:app/controllers/tweets_controller
ヒント:createの引数の形式に注意して、引数の末尾に保存するカラム名と保存したい値を書き加える。

ツイート保存時にユーザー情報も保存できたか確かめる

投稿したユーザーのidをテーブルに保存する処理を記述できたら、実際にそれがうまく機能するか確かめる。実際にツイートを投稿し、Sequel Proに保存されたレコードを確認。user_idに現在ログイン中のユーザーのidが保存されていれば成功。

保存できたか確認

 

8:マイページを作成

現在、ツイートの一覧画面には全ユーザーのツイートが表示されている。そこで、一覧画面とは別に、現在ログイン中のユーザーの投稿のみが表示されているマイページを作成する。

 作業内容

1.マイページのルーティングを記述する
2.コントローラとアクションを作成する
3.マイページ用のビューファイルを作成する
4.マイページへのリンクを作成する

1.マイページのルーティングを記述

新しくページを作成する場合、まずはルーティングを設定する。マイページへのパスは、usersテーブルでのidが1のユーザーでは/users/1、idが99のユーザーでは/users/99となるようにルーティングを設定する。

マイページのパスは他のページと違って、ユーザーごとにパスが異なる。その場合、通常とは多少異なるルーティングの記述方法を使う。
マイページを表示する際にはusersコントローラshowアクションを動かす。showアクションへのルーティングは、以下のように書く。

config/routes.rb
1
2
3
  Rails.application.routes.draw do
    get     '/コントローラ名/:id'        => 'コントローラ名#show'
  end

:idの部分には表示するユーザーページのユーザーのidが入る。

config/routes.rb
1
2
3
4
5
6
7
8
  Rails.application.routes.draw do
    devise_for :users
    root  'tweets#index'
    get   'tweets'      =>  'tweets#index'
    get   'tweets/new'  =>  'tweets#new'
    post  'tweets'      =>  'tweets#create'
    get   'users/:id'   =>  'users#show'    #Mypageへのルーティング
  end

このようにルーティングを定義することによって、パスの一部をコントローラにパラメーターというハッシュ形式で値を送る事ができる。これはparamsというメソッドを使用することで取得することができる。コントローラ内でparams[:id]と記述することにすれば/users/:id:id部分の情報を使用することができる。

2.コントローラとアクションを作成

usersコントローラを作成し、showアクションを定義していく。

 whereメソッド

whereメソッドはActiveRecordメソッドのうちの一つ。モデル.where(条件)のように引数部分に条件を指定することで、テーブル内の条件に一致したレコードのインスタンスを配列型で取得できる。

また、whereメソッドを連続して記述することによって、複数の条件に一致したレコードを取得することもできる。

コンソール
1
2
3
4
5
6
7
  [1] pry(main)> Tweet.where('id < 3')
  => [#<Tweet id: 1, image: "test1.jpg", text: "いい景色だ。", created_at: "2014-12-06 00:00:00", updated_at: "2014-12-06 00:00:00", user_id: 1>,#<Tweet id: 2, image: "test2.jpg", text: "Thank you!", created_at: "2014-12-07 00:00:00", updated_at: "2014-12-07 00:00:00", user_id: 2>]
  # idが3未満のtweetsテーブルのインスタンスを配列で取得

  [2] pry(main)> Tweet.where('id < 3').where(user_id: 1)
  => [#<Tweet id: 1, image: "test1.jpg", text: "いい景色だ。", created_at: "2014-12-06 00:00:00", updated_at: "2014-12-06 00:00:00", user_id: 1>]
  # idが3未満かつuser_idが1のtweetsテーブルのインスタンスを配列で取得
ターミナル
1
2
  $ rails g controller users
  # users_controllerと関連するファイルを作成

 新規作成されるファイル

  • app/controllers/users_controller.rb
  • test/controllers/users_controller_test.rb
  • app/helpers/users_helper.rb
  • test/helpers/users_helper_test.rb
  • app/assets/javascripts/users.js.coffee
  • app/assets/stylesheets/users.css.scss

続いて、作成したusers_controllerにshowアクションを定義する。showアクションで表示するのはユーザーのマイページだが、マイページに必要な情報はニックネームとログイン中のユーザーのツイートの2つ。そこで、showアクションにはユーザーページを表示する際に必要な情報をビューに受け渡す処理を記述する。

ビューに情報を受け渡す際にはインスタンス変数(@付きの変数)をコントローラのアクション内で定義する。今回の場合、必要な情報はニックネームとログイン中のユーザーのツイートの2つ。それぞれを@nickname@tweetsという変数に定義する。

 問題8:マイページに表示したい情報をshowアクションに定義

作業ファイル:app/controllers/users_controller
ヒント:@tweetsを定義する際はActiveRecordメソッドのwhereを使用。
users_controller.rb
1
2
3
4
5
6
7
8
 
  class UsersController < ApplicationController
    def show
      @nickname = 現在ログインしているユーザーのニックネーム
      @tweets = 現在ログインしているユーザーが投稿したツイート.page(params[:page]).per(5).order("created_at DESC")
    end
  end

3.マイページ用のビューファイルを作成

usersコントローラshowアクションを定義することができたので、アクションに対応するビューファイルを作成していく。

①ファイルの新規作成

先ほどusers_controllerを作成した際にapp/views/usersディレクトリが作成されているので、その中にshow.html.erbというファイルを作成。

 新規作成されるファイル

  • app/views/users/show.html.erb

②ファイルの編集

app/views/users/show.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <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) %>
        <span class="name"><%= tweet.name %></span>
      </div>
    <% end %>
    <%= paginate(@tweets) %>
  </div>

4.マイページへのリンクを作成

マイページへはPicTweetのページ上部から遷移できるように実装する。

1.マイページのルーティングを記述するで実装した通り、マイページへのパスは/users/ログイン中のユーザーのid。こちらをどう記述するかが鍵になる。

 問題9:マイページへのリンクを作成する

作業ファイル:app/views/layouts/application.html.erb
ヒント:マイページのパスは「/users/ログイン中のユーザーのid」となっている。そのため、ビューファイルもログイン中のユーザーによってリンクが変化するようにRubyタグを使って記述を行う。
【例】
app/views/layouts/application.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
  <header class="header">
    <div class="header__bar row">
      <h1 class="grid-6"><a href="/">PicTweet</a></h1>
      <% if user_signed_in? %>
        <div class="user_nav grid-6">
          <span>ログイン中のユーザーのニックネームを記述
            <ul class="user__info">
              <li>
                <a href="/users/ログイン中のユーザーのid">マイページ</a>
                <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
              </li>
            </ul>
          </span>
          <a class="post" href="/tweets/new">投稿する</a>
        </div>
      <% else %>
        <div class="grid-6">
          <%= link_to "ログイン", new_user_session_path, class: 'post' %>
          <%= link_to "新規登録", new_user_registration_path, class: 'post' %>
        </div>
      <% end %>
    </div>
  </header>
 

localhost:3000/users/ ログイン中のユーザーのid にアクセスしてマイページが作成されているか確認

マイページ確認

最後にルーティングを設定したところでparamsの中を確認する。binding.pryを使用。

users_controller.rb
1
2
3
4
5
6
7
class UsersController < ApplicationController
  def show
    binding.pry
    @nickname = current_user.nickname
    @tweets = Tweet.where(user_id: current_user.id).page(params[:page]).per(5).order('created_at desc')
  end
end

編集したら、http://localhost:3000/users/ ログインしているユーザーのidにアクセスする。
(ログインしているユーザーのID番号が1ならば、http://localhost:3000/users/1 。)

ここでの「ログインしているユーザーのid」には、現在ログインしているユーザーのidを入力。

するとサーバーが停止するので、ターミナルを確認する。

ターミナル
1
2
3
4
5
    2: def show
 => 3:   binding.pry
    4:   @nickname = current_user.nickname
    5:   @tweets = current_user.tweets.page(params[:page]).per(5).order('created_at desc')
    6: end

この状態で、paramsと入力。

ターミナル
1
2
[1] pry(#<UsersController>)> params
=> {"controller"=>"users", "action"=>"show", "id"=>"1"}

paramsというハッシュの中に"id" => "1"というルーティングで設定したキーとビューでパスに組み込まれたユーザーのidが追加されているのが確認できる。このparamsがコントローラに送られてくるのでコントローラ内でparams[:id]とすることでユーザーのidを取得することができる。また、ルーティングで:id:user_idのように変更すると、以下のようにキーがuser_idになる。

ターミナル
1
2
[1] pry(#<UsersController>)> params
=> {"controller"=>"users", "action"=>"show", "user_id"=>"1"}

 users_controller.rbに記述したbinding.pryを削除する

9:アソシエーションを利用

Railsにはモデル間の関係を管理するアソシエーションという機能がある。今回の作業ではこのアソシエーションという機能を実装する。

 アソシエーション

アソシエーションとはモデル間の関連付けを管理する機能のことで、定義しておくことでモデルをまたいだデータの呼び出しをより簡単に行うことができるようになる

今回の場合、UserモデルとTweetモデルをアソシエーションで関連付ける。下のヒントを参考にしながら実装する。

 作業内容

1.アソシエーションを定義する
2.アソシエーションを実装する

1.アソシエーションを定義

アソシエーションを利用するために、まずはモデル間の関係を定義する。アソシエーションを利用するには以下の2つの条件を満たす必要がある。

① モデルクラスにhas_manybelongs_toなどの定義がされている

② 所属する側のテーブルに所属するクラス名_idというカラムがある

 

現在PicTweetにはサインアップという機能があり、利用者がユーザーを作成してログインを行うことができる。また、ツイートの投稿に関してはログインしたユーザーのみが行うことができる。

つまり、全てのツイートはいずれかのユーザーが作成したものであり、逆に言えばユーザーは自分の作成したツイートを複数個所持している状態といえる。

アソシエーション説明

Userモデルの視点で考えると、あるuserの作成したtweetが複数個ある状態と言える。この状態のことをhas manyの関係といい、今回の場合には「User has many Tweets」の状態であると言える。

User_has_many_Tweets

この関係をアソシエーションで定義するためにはUserモデルのモデルファイルを編集する。「User has many Tweets」の状態を定義するので、Userモデルのモデルファイルにhas_many :tweetsと追記。

1
2
3
4
5
  class User < ApplicationRecord
    devise :database_authenticatable, :registerable,
           :recoverable, :rememberable, :validatable
    has_many :tweets
  end

また、Tweetモデルの視点で考えると、全てのtweetはいずれかのuserに属している状態と言える。この状態のことをbelongs toの関係といい、今回の場合は「Tweet belongs to User」の状態であると言える。

Tweet_belongs_to_User

この関係をアソシエーションで定義するためにはTweetモデルのモデルファイルを編集する。「Tweet belongs to User」の関係を定義するので、Tweetモデルのモデルファイルにbelongs_to :userと追記。

app/models/tweet.rb
1
2
3
  class Tweet < ApplicationRecord
    belongs_to :user
  end

2.アソシエーションを実装

以上の作業でアソシエーションを定義することができた。そこで、実際にアソシエーションを利用して実装を行っていく。さきほど編集を行ったusers_controller.rbshowアクション部分でのデータの呼び出しにアソシエーションを使用して記述を行う。

アソシエーションを定義すると、モデルをまたいだデータの呼び出しをより直感的に行うことができる。便利さを実感してもらうために、アソシエーションを使用しない場合とする場合のコードの違いを見てみる。

アソシエーションを使用しない場合は、以下のように現在ログイン中のユーザーが投稿したツイートを取得する。
例としてコンソールからのデータの呼び出しを行う。

アソシエーションを使用しない場合

ターミナル
1
2
3
4
5
  $ rails c
  [1] pry(main)> user = User.find(1)
  [2] pry(main)> Tweet.where(user_id: user.id)
  => [#<Tweet id: 1, image: "http://photo1.jpg", text: "いい景色だ。", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", user_id: 1>,
  #<Tweet id: 1, image: "http://photo2.jpg", text: "楽しい思い出は消えない。", created_at: "2014-12-06 10:00:00", updated_at: "2014-12-06 10:00:00", user_id: 1>]

tweetsテーブルに対して、user_iduserのidと等しいレコードを取得している。

対して、これと同じくアソシエーションを利用することで以下のように記述できる。

アソシエーションを使用する場合

1
2
3
4
5
  $ rails c
  [1] pry(main)> user = User.find(1)
  [2] pry(main)> user.tweets
  => [#<Tweet id: 1, image: "http://photo1.jpg", text: "いい景色だ。", created_at: "2014-12-06 09:00:00", updated_at: "2014-12-06 09:00:00", user_id: 1>,
  #<Tweet id: 1, image: "http://photo2.jpg", text: "楽しい思い出は消えない。", created_at: "2014-12-06 10:00:00", updated_at: "2014-12-06 10:00:00", user_id: 1>]

Userモデルに「User has many Tweets」の状態のアソシエーションを定義したので、Userモデルのインスタンス.tweetsと記述するだけでそのインスタンスが所持しているツイートを取得することができる。

users_controllerのshowアクション内を編集してアソシエーションを利用してみる。

 問題10:アソシエーションを利用して@tweetsを定義する

作業ファイル:app/controllers/users_controller
ヒント:現在ログイン中のユーザーがつぶやいたツイート達を取得するよう書く
users_controller.rb
1
2
3
4
5
6
class UsersController < ApplicationController
  def show
    @nickname = current_user.nickname
    @tweets = アソシエーションを使用して記述.page(params[:page]).per(5).order("created_at DESC")
   end
end

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

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

 作業内容

1.投稿者名を表示するようにビューを変更する
2.ツイートからユーザー情報を先読みする
3.投稿画面のビューを変更する
4.投稿時のコントローラでの処理を変更する
5.tweetsテーブルから不要なカラムを削除する

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

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

Tweetモデルに対して「Tweet belongs to User」という形でアソシエーションを定義しているので、Tweetモデルのインスタンス.userと記述するだけでそのインスタンスが属しているUserモデルのインスタンスを取得することができる。

アソシエーションを利用しない場合

ターミナル
1
2
3
4
  $ 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がtweetuser_idと等しいレコードを取得している。これと同じくアソシエーションを利用することで、以下のように記述できる。

アソシエーションを利用する場合

ターミナル
1
2
3
4
  $ 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  <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>

 sequelproを開き、tweetsテーブルのuser_idの値を、全て現在ログインしているユーザーのid番号に変更する

ビューを確認する前に、以下、注意事項がある。

 Sequel Pro上で、tweetsテーブルにあるレコードのうちuser_idカラムが空のものに、現在存在しているusersテーブルのレコードのidを入力しているか確かめる。

7で、tweetを投稿した際にuser_idも一緒に保存するようにしたが、Sequel Proで確認すればわかる通り、それ以前に投稿したtweetuser_idnullのままになっているはず。この時tweet.usernilとなるため、tweet.user.nicknameと書くと、nullクラスに対してnicknameメソッドが実行されてしまいエラーとなる。

 localhost:3000にアクセスして、投稿一覧に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テーブルのレコードも一度に取得することができる。

includes

app/controllers/tweets_controller.rb
1
2
3
4
5
6
7
8
9
  class TweetsController < ApplicationController

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

    #以下省略

  end

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

ツイートを表示する際にアソシエーションを利用して投稿者のニックネームが表示されるようになったので、投稿時に「name」を入力する必要がなくなった。そこで、この実装に合わせたビューの変更を行いる。投稿時に「name」を入力する必要がなくなったので、「name」の入力フォームを削除する。

app/views/tweets/new.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="contents row">
  <%= form_tag('/tweets', method: :post) do %>
    <h3>
      投稿する
    </h3>
    <input placeholder="Image URL" type="text" name="image" autofocus="true">
    <textarea placeholder="text" name="text" cols="30" rows="10"></textarea>
    <input type="submit" value="SENT">
  <% end %>
</div>

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

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

app/controllers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  class TweetsController < ApplicationController

    before_action :move_to_index, except: :index

    def index
      @tweets = Tweet.includes(:user).page(params[:page]).per(5).order("created_at DESC")
    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」カラムが不要になった。
そこで、テーブルからカラムを削除するためのマイグレーションファイルを作成して、カラムの削除を実行する。

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

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

ターミナル
1
2
3
4
5
  $ rails g migration RemoveNameFromTweets name:string
  # マイグレーションファイルの作成

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

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

 新規作成されるファイル

  • db/migrate/2015XXXXXXXXXX_remove_name_from_tweets.rb

 

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

app/views/users/show.html.erb
1
2
3
4
5
6
7
8
9
  <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>

11:ツイートの削除機能を追加

現在の実装では一度投稿したツイートは削除することができない。そこで、ツイートに削除ボタンを追加して、ツイートの削除を行えるようにする。ツイートを削除する流れは、下記の図のような流れで実行される。

delete.png

作業手順はルーティングを設定することでビューに削除のパスを指定できるため、ルーティングの編集から始まる。

 作業内容

1.ツイートを削除するためのルーティングを設定する
2.削除ボタンをビューに追加する
3.destroyアクションをコントローラに定義する
4.削除後のビューを追加する

1.ツイートを削除するためのルーティングを設定

ツイート削除機能を実装するためにまずはルーティングを定義している。ツイートを削除する際にはツイートごとに固有のパスが発行されるので、ユーザーのマイページを作成したときと同様に少し特別なルーティングが必要になる。

また、ツイートの削除を行う際にはdeleteというHTTPメソッドを利用する。

 deleteメソッド

deleteメソッドは情報の削除を行う際に利用するHTTPメソッド。今回の場合、ツイートの削除を行うのでdeleteメソッドを利用する。

また、ツイートの削除を行う際には、tweetsコントローラdestroyアクションを動かす。

config/routes.rb
1
2
3
4
5
6
7
8
9
  Rails.application.routes.draw do
    devise_for :users
    root  'tweets#index'
    get   'tweets'      =>  'tweets#index'
    get   'tweets/new'  =>  'tweets#new'
    post  'tweets'      =>  'tweets#create'
    delete  'tweets/:id'  => 'tweets#destroy'
    get   'users/:id'  =>  'users#show'
  end

リクエストする箇所(ビュー)では、パスの:idの部分に削除するツイートのidが入るように記述。すると、パラメーターがコントローラに渡る際paramsのキーにidが追加され、そのバリューはリクエストの際に:idの部分に記述された値になる。つまり、:idの部分に記述された値はコントローラ上でparams[:id]とすることで取得できる。

2.削除ボタンをビューに追加

ツイートを削除するためにビューファイルを編集して削除ボタンを追加する。今回は、link_toメソッドを使用。削除ボタンはツイートの表示画面の右上に配置される。また、自分が投稿したツイートにのみ表示されるようにする。

link_toメソッドを実装する際にはrake routesコマンドを使用して、パスを確認する必要がある。

必ずしもrake routesで確認する必要性はなく、指定するパスを忘れてしまった場合にコマンドを実行する。

 rake routes

このコマンドは現在定義しているルーティングを表示してくれるコマンド。ターミナルで入力することで、config/routes.rbに記述したルーティングを表示してくれる。

ターミナル
1
  $ rake routes
出力結果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
           user_password POST   /users/password(.:format)      devise/passwords#create
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
                         PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
       user_registration POST   /users(.:format)               devise/registrations#create
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
                         PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                    root GET    /                              tweets#index
                  tweets GET    /tweets(.:format)              tweets#index
                         POST   /tweets(.:format)              tweets#create
               new_tweet GET    /tweets/new(.:format)          tweets#new
                   tweet DELETE /tweets/:id(.:format)          tweets#destroy
                    user GET    /users/:id(.:format)           users#show

rake routesコマンドを実行すると例えば上記のような出力結果が表示される。(完全に同様の出力結果でなくても構わない。)この出力結果は以下のような意味を持っている。

項目名 意味
Prefix 「Prefix」はルーティングの名前を表しています。「Prefix」は設定を行わなくても、自動的に割り当てられますが、自ら設定することもできる。
Verb 「Verb」はHTTPメソッドを表している。右側に表示されているパスにどのHTTPメソッドでアクセスするかを示している。
URI Pattern

URI Pattern」はルーティングのパスを表している。このパスにアクセスされた際に、指定のコントローラとアクションで処理が行われる。

Controller#Action 「Controller#Action」は指定したパスにアクセスした際に処理が行われるコントローラとアクションを表している。#以前がコントローラ名、#以後がアクション名を示している。



また、/tweets(.:format) の後ろについている(.:format)とは、formatオプションとよばれるもので、html形式やjson形式といった複数の形式で出力することができるというもの。

 問題11:削除ボタンを作成する

作業ファイル:app/views/tweets/index.html.erb
ヒント1:下記の例をもとに、link_toメソッドを利用する。
ヒント2:rake routesで確認した、tweetsコントローラのdestroyメソッドを動かすhttpメソッド/パスの組み合わせを書く
ヒント3:Rubyタグの中の文字列内で変数を使いたい場合は式展開#{}を使用する。詳しくはトラブルシューティング(Rails編)で確認する。
ヒント4:削除したいツイートを投稿したユーザーのidは変数`tweet`を使用して取得する。
【例】
index.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    //省略
  <div class="content_post" style="background-image: url(<%= tweet.image %>);">
    <% if ユーザーがログインしているか? かつ current_user.id == 削除したいツイートを投稿したユーザーのid %>
      <div class="more">
        <span><%= image_tag 'arrow_top.png' %></span>
        <ul class="more_list">
          <li>
            <%= link_to '削除', "/tweets/削除したいツイートのid", method: :HTTPメソッドを指定 %>
         </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>
 //省略

3.destroyアクションをコントローラに定義

ツイートを削除する際にはtweetsコントローラdestroyアクションを動かす。そこで、このアクションをコントローラに定義していく。

 destroyメソッド

ActiveRecordメソッドのうちの一つで、インスタンスに対してそのレコードを削除する際に使うことができる。ターミナルで実行して確認する。

ターミナル
1
2
3
4
  $ rails c
  [1] pry(main)> tweet = Tweet.find(今あるtweetsテーブルのid)
  => #<Tweet:0x007ff621357720 id: 1, name: "george", text: "いい景色だ", image: "http://photo1.jpg", created_at: Thu, 16 Apr 2015 05:41:20 UTC +00:00, updated_at: Thu, 16 Apr 2015 05:41:20 UTC +00:00, user_id: 1>]
  [2] pry(main)> tweet.destroy

すると、以下のような結果になり、レコードが削除される。

ターミナル
1
2
3
4
     (0.2ms)  BEGIN
    SQL (1.6ms)  DELETE FROM `tweets` WHERE `tweets`.`id` = 1
     (0.3ms)  COMMIT
  => #<Tweet:0x007ff621357720 id: 1, name: "george", text: "いい景色だ", image: "http://photo1.jpg", created_at: Thu, 16 Apr 2015 05:41:20 UTC +00:00, updated_at: Thu, 16 Apr 2015 05:41:20 UTC +00:00, user_id: 1>]

では、tweets_controllerdestroyアクションを実装していく。 ツイートを削除する際にはdestroyメソッドを使う。また、どのツイートを削除するのかを特定する場合はparams[:id]を使用して、削除したいツイートの情報を取得する。

削除を行えるのは、そのツイートを投稿したユーザーのみ。そのため、別のユーザーが削除をできないように条件をつける必要がある。

app/controllers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  class TweetsController < ApplicationController

    before_action :move_to_index, except: :index

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

    def new
    end

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

    def destroy
      tweet = Tweet.find(params[:id])
      if tweet.user_id == current_user.id
        tweet.destroy
      end
    end

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

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

 後置if

最後のendを省略してif文を処理の後方に配置する書き方。
elsif, elseにあたる条件分岐が無く、かつ処理が一行で完結する場合に用いる。

コード量も少なくシンプルに記述できるので、今回のdestroyメソッドは以下のように書き直す。

app/controllers/tweets_controller.rb
16
17
18
19
    def destroy
      tweet = Tweet.find(params[:id])
      tweet.destroy if tweet.user_id == current_user.id
    end

4.削除後のビューを追加

ルーティングとコントローラの編集が終わったので、続いてビューファイルの追加を行う。このビューファイルはツイートの削除を行ったあとに呼び出されるビューファイルになる。

①ファイルの新規作成

app/views/tweetsディレクトリの中にdestroy.html.erbというファイルを作成

②ファイルの編集

作成したファイルを、以下のように編集。

app/views/tweets/destroy.html.erb
1
2
3
4
5
6
7
8
  <div class="contents row">
    <div class="success">
      <h3>
        削除が完了しました。
      </h3>
      <a class="btn" href="/">投稿一覧へ戻る</a>
    </div>
  </div>

 localhost:3000にアクセスして、削除機能を試してみる

① ログインする。

② ログインしたユーザーでツイートの投稿する。一番上に表示される。

投稿確認

③ 削除ボタンをクリックする。

削除ボタンを押す

④ 削除完了画面が表示される。

削除確認画面

⑤ トップページに戻り、②のツイートがなくなっていれば、削除機能は実装完了。

投稿一覧画面

12:ツイートの編集機能を追加

編集を行う際には編集ページを用意して、そのページから編集を行うようにする。

 作業内容

1.ツイート編集画面のルーティングを設定する
2.編集ボタンをビューに追加する
3.editアクションをコントローラに定義する
4.編集画面のビューファイルを作成する
5.ツイートの更新を行うためのルーティングを設定する
6.updateアクションをコントローラに定義する
7.更新後のビューファイルを追加する

1.ツイート編集画面のルーティングを設定

ツイートを編集する際は、ツイートを編集するための画面に遷移したあとに、ツイートの編集を行う。今回の作業では、編集画面へ遷移するためのルーティングを設定する。

編集画面への遷移にはコントローラtweetsコントローラeditアクションを動かす。また、編集画面のパスはツイートごとに異なるものになるので、destroyメソッドの時と同じように:idのような書き方を利用する。

config/routes.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  Rails.application.routes.draw do
    devise_for :users
    root  'tweets#index'
    get   'tweets'      =>  'tweets#index'
    get   'tweets/new'  =>  'tweets#new'
    post  'tweets'      =>  'tweets#create'
    delete  'tweets/:id'  => 'tweets#destroy'
    get   'tweets/:id/edit'  => 'tweets#edit'
    get   'users/:id'  =>  'users#show'
  end

2.編集ボタンをビューに追加

ツイートを編集する画面に遷移するためのボタンを作成する。編集ボタンは、削除ボタンの上に表示されるようする。削除ボタンと同じように、link_toメソッドを使用する。

app/views/tweets/index.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <div class="content_post" style="background-image: url(<%= tweet.image %>);">
      <% if user_signed_in? && tweet.user_id == current_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>
  <% end %>
  <%= paginate(@tweets) %>
</div>

3.editアクションをコントローラに定義

ツイートを編集する画面を表示する際にはtweetsコントローラeditアクションを使用する。そこで、コントローラにeditアクションを作成していく。

現在実装している編集して更新、という流れは、入力して新規投稿、という流れと似ている。新規作成時と違うのは、編集→更新の場合は既に存在しているレコードを選択して中身を書き換えるという点。そのため、editアクションでは編集したいレコードを@tweetに代入し、編集画面で利用できるようにする。

app/controllers/tweets_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  class TweetsController < ApplicationController

    before_action :move_to_index, except: :index

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

    def new
    end

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

    def destroy
      tweet = Tweet.find(params[:id])
      tweet.destroy if tweet.user_id == current_user.id
    end

    def edit
      @tweet = Tweet.find(params[: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

4.編集画面のビューファイルを作成

ルーティングとコントローラの編集が終わったので、ビューファイルを作成する。このビューはツイートを編集するためのビューファイルである。そのため、ツイートの投稿画面と同様に情報を入力するためのフォームが必要になる。

入力情報の送信先/tweets/編集するツイートのidとなる。また、情報を送信する際にはpatchというHTTPメソッドを使用する。

 patchメソッド

HTTPリクエストのpatchメソッドは情報の更新を行う際に使用する。今回はツイート情報を更新する機能を実装するので、patchメソッドを使用する。

 HTTPリクエス

Railsでは以下の4つのメソッドを利用して開発を行う。

メソッド 役割
get サーバーからブラウザに情報を返す。単にウェブサイトを閲覧する際にはこのメソッドが利用されている。
post ブラウザからサーバーに情報を送信し、サーバーに情報を保存する。情報の登録などの際に、サーバーに情報を送信するために利用される。
delete ブラウザからサーバーに情報を送信し、サーバーの情報を削除する。アカウントの削除などの際に、サーバー内のデータを削除するために利用される。
patch ブラウザからサーバーに情報を送信し、サーバー内の情報を置き換える。登録情報の更新などの際に、サーバー内のデータを更新するために利用される。
put メソッド名は違うが、patchと同じ役割を持っている。rails3まではputを使っていたが、rails4からはpatchに移行したためカリキュラム内では使用しない。

フォームを作成する際にはフォームの初期値として、編集前のツイート情報を表示する。

①ファイルの新規作成

app/views/tweetsディレクトリの中にedit.html.erbというファイルを作成

②ファイルの編集

 問題12:編集画面を作成する

作業ファイル:app/views/tweets/edit.html.erb
ヒント1:下記の例をもとに編集画面を作成する。
ヒント2:このビューでは、コントローラで定義した@tweetを利用できます。@tweetには、編集したいレコードが代入されている。
ヒント3:form_tagメソッドのあたりは、tweetsのnewアクションのビューを参考にする。
ヒント4:編集前の値を表示するには、タグを利用する必要がある。
【例】
edit.html.erb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <div class="contents row">
    form_tagメソッドを使って「/tweets/編集するツイートのid」に「patch」メソッドで情報を送信
      <h3>
        編集する
      </h3>
      <input placeholder="Image Url" type="text" name="image" value=編集前の初期値を表示 autofocus="true">
      <textarea cols="30" name="text" placeholder="text" rows="10">編集前の初期値を表示</textarea>
      <input type="submit" value="SENT">
    <% end %>
  </div>

5.ツイートの更新を行うためのルーティングを設定

ツイートの更新を行うためのルーティングを設定していく。

ツイートを更新する際には/tweets/編集するツイートのidpatchメソッドでアクセスする。このパスにアクセスした際にtweetsコントローラupdateアクションが動くようにする。

config/routes.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  Rails.application.routes.draw do
    devise_for :users
    root  'tweets#index'
    get   'tweets'      =>  'tweets#index'
    get   'tweets/new'  =>  'tweets#new'
    post  'tweets'      =>  'tweets#create'
    delete  'tweets/:id'  => 'tweets#destroy'
    patch   'tweets/:id'  => 'tweets#update'
    get   'tweets/:id/edit'  => 'tweets#edit'
    get   'users/:id'  =>  'users#show'
  end

6.updateアクションをコントローラに定義

ツイートを更新する際にはtweetsコントローラupdateアクションを使用。updateアクション内ではActiveRecordメソッドのupdateを使用する。

 updateメソッド

updateActiveRecordメソッドのうちの一つ。updateメソッドはモデルのインスタンスに対して使用することで、引数内の情報にレコードを更新することができる。

コンソール
1
2
3
4
5
6
  [1] pry(main)> tweet = Tweet.create(user_id: 1, text: "Hello", image: "test.jpg")
  => #<Tweet id: 1, user_id: 1, text: "Hello", image: "test.jpg", created_at: "2014-12-10 00:00:00", updated_at: "2014-12-10 00:00:00">
  [2] pry(main)> tweet.update(text: "Thank you!")
  => true
  [3] pry(main)> tweet
  => #<Tweet id: 1, user_id: 1, text: "Thank you!", image: "test.jpg", created_at: "2014-12-10 00:00:00", updated_at: "2014-12-10 00:01:00">

updateメソッドの引数は、update(カラム名: 更新する情報)という形で指定する。同時に複数の情報を更新する場合にはupdate(カラム1: 更新情報1, カラム2: 更新情報2)という形で使用することができる。createメソッドと似ている。

 問題13:updateアクションを実装する

作業ファイル:app/controllers/tweets_controller
ヒント1:destroyアクションの実装の時と同じような流れ。
ヒント2:ツイートを更新する際にはActiveRecordメソッドのupdateメソッドを使用する。また、updateメソッドの引数にはtweet_paramsを使用する。

7.更新後のビューファイルを追加

続いてビューファイルの追加を行う。このビューファイルはツイートの更新を行ったあとに呼び出されるビューファイル。

①ファイルの新規作成

app/views/tweetsディレクトリの中にupdate.html.erbというファイルを作成。

 新規作成されるファイル

  • app/views/tweets/update.html.erb

②ファイルの編集

app/views/tweets/update.html.erb
1
2
3
4
5
6
7
8
  <div class="contents row">
    <div class="success">
      <h3>
        更新が完了しました。
      </h3>
      <a class="btn" href="/">投稿一覧へ戻る</a>
    </div>
  </div>

 

 localhost:3000にアクセスして編集機能を試す

① ログインする。

② ツイートを投稿する。

③ ②で投稿したツイートは一番上にあるので、そのツイートの編集ボタンをクリックする。

編集ボタン

④ 編集画面が表示されるので、テキストを変更する。

編集画面

⑤ 更新完了画面が表示される。

投稿完了画面

⑥ トップページに戻り、一番上のツイートのテキストが変更されていることを確認。

投稿一覧画面

⑦ これで編集機能実装は完了。

 Railsのリソースの7つのアクション

Railsでは、7つの基本アクションへのリクエストが下記の通りデフォルトで設定されている。これらのリクエストは、routes.rbresources :ルーティングを設定したいコントローラ名と書くだけで反映させることができる。

アプリケーションの作成において何か機能を追加したい時は、まずはこの7つのアクションに当てはまる機能かを考え、そうであれば積極的に利用する。ルーティングを楽に設定できると同時に、他の人がコードを読む際わかりやすいという利点がある。

Railsのリソースの

7つのアクション

役割
index リソースの一覧を表示する。
show リソースの内容を表示する。
new リソースを追加する。
create リソースを追加し、作成する。
edit リソースを更新するためのフォームを表示する。
update リソースを更新する。
destroy リソースを削除する。