ぞえの技術めも

Ruby on Rails勉強中

【182日目】【1日20分のRailsチュートリアル】【第12章】SQLサブセレクトを使用する

Ruby on Railsチュートリアル(第3版)

今日は「12.3.3 サブセレクト」から。

12.3.3 サブセレクト

前節のヒントでおわかりのように、12.3.2のフィードの実装は、投稿されたマイクロポストの数が膨大になったときにうまくスケールアップできません。フォローしているユーザーが5000人程度になるとこういうことが起きる可能性があります。

あぁ、マイクロポストの読み込みというより、ユーザーの配列を作るのに大変なのか。

12.3.2で示したコードの問題は、following_idsでフォローしているすべてのユーザーをメモリーから一気に取り出し、フォローしているユーザーの完全な配列を作り出したことです。
(中略)
これを解決する方法は、フォローしているユーザーのidの検索をデータベースに保存するときにサブセレクト (subselect) を使用することです。

サブセレクト…???

[SQL] 7. サブクエリ 1 | TECHSCORE(テックスコア)

複数のクエリを組み合わせて、1つのクエリが生成した出力で、他のクエリの出力を制御することができます。少し分かりやすく言うと、クエリを入れ子にして、内側のクエリが値を生成し、それを外側のクエリの述語が評価して TRUE かどうか判断します。

どうもSQLの用語で「サブクエリ」とも言うっぽい。
複数のクエリを組み合わせることか…。

リスト12.45でコードを若干修正し、フィードをリファクタリングすることから始めましょう。

ちょっと変わった。

app/models/user.rb

  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                    following_ids: following_ids, user_id: id)
  end

前者の疑問符を使用した文法も便利ですが、同じ変数を複数の場所に挿入したい場合は、後者の置き換え後の文法を使用するのがより便利です。

今は一箇所しか挿入してないけど、これから増やすのか。だからこっちのが便利なんだね。

このサブセレクトは、集合のロジックを (Railsではなく) データベースに保存するので、より効率が高まります。

へー…?そうなのか。なんかぱっと見変わってないんだけどこれで効率良くなるのか。

Rubyコード使うよりSQL使う方が効率いいってことなのかー。

app/models/user.rb

  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE  follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end

テストは特に問題なし。

$ bundle exec rake test
74 tests, 371 assertions, 0 failures, 0 errors, 0 skips

もちろん、サブセレクトを使用すればいくらでもスケールアップできるなどということはありません。大規模なWebサイトでは、バックグラウンドジョブを使用して、フィードを非同期で生成するなどの対策が必要でしょう。Webサイトのスケーリングのようなデリケートな問題は本書の範疇を超えます。

あ、その辺りは扱わないのか…。Twitterとかは非同期なのかもね。Ajax使ったりするサイトもありそう。
まぁこれは「チュートリアル」だからね。Webサイトって奥が深い。。

今日の学習時間は【21分】

次は「12.3.3 サブセレクト」のHomeページにフィードを追加するところから。

【181日目】【1日20分のRailsチュートリアル】【第12章】フィードを初めて実装する

Ruby on Railsチュートリアル(第3版)

今日は「12.3.2 フィードを初めて実装する」から。

12.3.2 フィードを初めて実装する

最初に、このフィードで必要なクエリについて考えましょう。ここで必要なのは、micropostsテーブルから、あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) することです。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

そうですね。こうなりますね。

これらの条件から、フォローされているユーザーに対応するidの配列が必要であることがわかってきました。これを行う方法の1つは、Rubyのmapメソッドを使用することです。

検索のために配列を用意する、と。

実は、?を内挿すると自動的にこの辺りの面倒を見てくれます。さらに、データベースに依存する一部の非互換性まで解消してくれます。つまり、ここではfollowing_idsメソッドをそのまま使えばよいだけなのです。

なんか色々説明あったけどfollowing_idsを使うだけでいいらしい…!便利だな…

app/models/user.rb

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

これだけでテスト通った!
結果だけ見ればすっきりしてるけど、ここに至るまでの過程も理解しておくと自分で作るとき参考になりそう。

$ bundle exec rake test
74 tests, 371 assertions, 0 failures, 0 errors, 0 skips

しかしリスト12.43にはまだ足りないものがあります。それが何なのか、次の節に進む前に考えてみてください(ヒント:フォローしているユーザーが5000人もいたらどうなるでしょうか)。

データベースからの読み込みにめっちゃ時間かかるね。アプリケーションの動き遅くなるね。多分ね。
ページング的な処理を入れるのかな…??

今日の学習時間は【15分】

次は「12.3.3 サブセレクト」から。

【180日目】【1日20分のRailsチュートリアル】【第12章】ステータスフィードのテストを作成する

Ruby on Railsチュートリアル(第3版)

今日は「12.3 ステータスフィード」から。

12.3 ステータスフィード

ついに、サンプルアプリケーションの山頂が目の前に現れました。最後の難関、ステータスフィードの実装に取りかかりましょう。
(中略)
これ実現するためには、RailsRubyの高度な機能の他に、SQLプログラミングの技術も必要です。

今まで作ったものを組み合わせるだけじゃないのか…!
そうか、色んなユーザーのマイクロポストが混ざったフィードを作らないといけないのか。それは今までなかったね。

12.3.1 動機と計画

この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すことです。

フィードにはあるユーザーがフォローしているユーザーとそのユーザー自身のマイクロポストを表示する。Twitterのタイムラインか。

このテストで重要なことは、フィードに必要な3つの条件を満たすことです。
1) フォローしているユーザーのマイクロポストがフィードに含まれていること。2) 自分自身のマイクロポストもフィードに含まれていること。3) フォローしていないユーザーのマイクロポストがフィードに含まれていないこと。

どう実装するかは決まってないけど、テストすることは明確なので先にテストを作成する。

テストユーザーのフォロー関係を意識して、michaelをメインにテストを書いていく感じかな。
post_followingとかってすでに定義したんだっけ…。忘れた…。

test/models/user_test.rb

  test "feed should have the right posts" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)
    # フォローしているユーザーの投稿を確認
    lana.microposts.each do |post_following|
      assert michael.feed.include?(post_following)
    end
    # 自分自身の投稿を確認
    michael.microposts.each do |post_self|
      assert michael.feed.include?(post_self)
    end
    # フォローしていないユーザーの投稿を確認
    archer.microposts.each do |post_unfollowed|
      assert_not michael.feed.include?(post_unfollowed)
    end
  end

フィードはまだ実装してないのでテストは失敗しますね。

$ bundle exec rake test
  :
 FAIL["test_feed_should_have_the_right_posts", UserTest, 2017-08-23 21:13:41 +0000]
 test_feed_should_have_the_right_posts#UserTest (1503522821.24s)
        Expected false to be truthy.
        test/models/user_test.rb:105:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:104:in `block in <class:UserTest>'
  :

Finished in 2.97709s
74 tests, 334 assertions, 1 failures, 0 errors, 0 skips

今日の学習時間は【23分】

次は「12.3.2 フィードを初めて実装する」から。

【179日目】【1日20分のRailsチュートリアル】【第12章】フォロー/フォロー解除をテストする

Ruby on Railsチュートリアル(第3版)

今日は「12.2.6 フォローをテストする」から。

12.2.6 フォローをテストする

ユーザーのフォローに対するテストでは、 /relationshipsに対してPOSTリクエストを送り、フォローされたユーザーが1人増えたことをチェックします。

Ajaxでのリクエスト発行もテストで書けるのか…!xhr (XmlHttpRequest) っていうメソッド使うらしい。

これらのテストをまとめた結果を、リスト12.39に示します。

普通のHTTPリクエストとAjaxでのPOST/DELETEリクエストをテストする。
リクエスト発行後、フォローされたユーザーが1人増えているか、フォローしているユーザーが1人減っているかを見る。

ふむ、結構シンプルなテスト。

test/integration/following_test.rb

  def setup
    @user = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end
  :
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, followed_id: @other.id
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      xhr :post, relationships_path, followed_id: @other.id
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      xhr :delete, relationship_path(relationship)
    end
  end

特に問題なくテスト通りました。

$ bundle exec rake test
73 tests, 333 assertions, 0 failures, 0 errors, 0 skips

今日の学習時間は【13分】

次は「12.3 ステータスフィード」から。

【178日目】【1日20分のRailsチュートリアル】【第12章】JavaScriptでフォロー/フォロー解除ボタンを更新する

Ruby on Railsチュートリアル(第3版)

今日は「12.2.5 [フォローする] ボタン (Ajax)」のJavaScriptが無効になっていた場合の対応を実装するところから。

12.2.5 [フォローする] ボタン (Ajax)

リスト12.35Ajaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合 (Ajaxリクエストが送れない場合) でもうまく動くようにします (リスト12.36)。

この一行でJavaScriptが無効になっていても上手く動作するのか…。

config/application.rb

    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true

一方で、JavaScriptが有効になっていても、まだ十分に対応できていない部分があります。
(中略)
ユーザーをフォローしたときやフォロー解除したときにプロフィールページを更新するために、私たちがこれから作成および編集しなければならないのは、まさにこれらのファイルです。

Ajaxリクエストを受信した場合は、Railsが自動的にアクションと同じ名前を持つJavaScript用の埋め込みRuby (.js.erb) ファイルを呼び出すらしいので呼び出されるファイルをまず生成しておく。

$ touch app/views/relationships/create.js.erb
$ touch app/views/relationships/destroy.js.erb

create.js.erbファイルでは、フォロー用のフォームをunfollowパーシャルで更新し、フォロワーのカウントを更新するのにERbを使用しています (もちろんこれは、フォローに成功した場合の動作です)。

フォローボタンを押下されたときはアンフォロー用のHTMLで置き換えて、フォロワー数を更新する。
フォロー解除ボタンを押下されたときはフォロー用のHTMLで置き換えて、フォロワー数を更新する。
って感じなのかー。

app/views/relationships/create.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

app/views/relationships/destroy.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

これらのコードにより、ユーザープロファイルを表示して、ページを更新せずにフォローまたはフォロー解除ができるようになったはずです。

サーバーを起動して動作見てみた。
スクショでは表現できないので貼らないけど、多分ページ更新してない!でもフォロー/フォロー解除できてる!
こっちの方が自然な感じでいいね。

$ rails server -b $IP -p $PORT

今日の学習時間は【28分】

次は「12.2.6 フォローをテストする」から。

【178日目】【1日20分のRailsチュートリアル】【第12章】Ajaxを使ったフォローボタンを考える

Ruby on Railsチュートリアル(第3版)

今日は「12.2.5 [フォローする] ボタン (Ajax)」から。

12.2.5 [フォローする] ボタン (Ajax)

ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのでしょうか。この点を考えなおしてみましょう。

フォローボタンを押した後、そのままだとフォローボタンはフォローボタンのまま。(フォロー解除ボタンにならない)
すぐリダイレクトするようにしているからこそボタンの描画が切り替わる。 これをAjaxを使用して更新しよう、ってことかなー。

たったこれだけで、Rails自動的にAjaxを使用します。更新の結果をリスト12.33リスト12.34に示します。

へー、一行で済むのか…。

app/views/users/_follow.html.erb

<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>

app/views/users/_unfollow.html.erb

  :
             remote: true) do |f| %>

上の (ブロック内の) コードのうち、いずれかの1行が実行されるという点が重要です (このためrespond_toメソッドは、上から順に実行する逐次処理というより、if文を使った分岐処理に近いイメージです)。

ややこしいね。忘れそう。

ユーザーのローカル変数 (user) をインスタンス変数 (@user) に変更した点に注目してください。これは、リスト12.32のときはインスタンス変数は必要なかったのですが、リスト12.33リスト12.34を実装したことにより、インスタンス変数が必要になったためです。

へー。インスタンス変数じゃないと動かないコードがあるのか…。
format.html { redirect_to @user }インスタンス変数が必要なのかな?

HTMLリクエストなら上のコード、Ajaxリクエストなら下のコードが動く、ってことなんだよね…難しい…。

app/controllers/relationships_controller.rb

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

今日の学習時間は【28分】

次は「12.2.5 [フォローする] ボタン (Ajax)」のJavaScriptが無効になっていた場合の対応を実装するところから。

【177日目】【1日20分のRailsチュートリアル】【第12章】フォローボタン押下時の処理を実装する

Ruby on Railsチュートリアル(第3版)

今日は「12.2.4 [フォローする] ボタン (標準的な方法)」から。

12.2.4 [フォローする] ボタン (標準的な方法)

フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要です。

むしろ今までコントローラ作ってなかったんだ…。そういえば作ってない…。

$ rails generate controller Relationships
      create  app/controllers/relationships_controller.rb
      invoke  erb
      create    app/views/relationships
      invoke  test_unit
      create    test/controllers/relationships_controller_test.rb
      invoke  helper
      create    app/helpers/relationships_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/relationships.coffee
      invoke    scss
      create      app/assets/stylesheets/relationships.scss

今回はまず、コントローラのアクションにアクセスするとき、ログイン済みのユーザーであるかどうかをチェックします。
もしログインしていなければ、ログインページにリダイレクトさせ、Relationshipのカウントが変わっていないことを確認します (リスト12.30)。

コントローラ実装していく前にテストから作成する。

ログインしていない場合はRelationshipコントローラのアクションにアクセスしようとしてもログインページにリダイレクトされるよ、ってテストなのか。

test/controllers/relationships_controller_test.rb

require 'test_helper'

class RelationshipsControllerTest < ActionController::TestCase

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post :create
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete :destroy, id: relationships(:one)
    end
    assert_redirected_to login_url
  end
end

次に、リスト12.30のテストをパスさせるために、logged_in_userフィルターをRelationshipsコントローラのアクションに対して追加します (リスト12.31)。

今はログイン状態を見てるだけなのでこの対応だけでもテストは通る、と。

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

フォロー/フォロー解除ボタンを動かすためには、フォーム (リスト12.21/リスト12.22) から送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要があります 。その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッド (リスト12.10) を使います。

実装されたもの見るとシンプルな作りだなぁ。

app/controllers/relationships_controller.rb

  def create
    user = User.find(params[:followed_id])
    current_user.follow(user)
    redirect_to user
  end

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end

これで、フォロー/フォロー解除の機能が完成しました。

テスト通してみる。

$ bundle exec rake test
69 tests, 329 assertions, 0 failures, 0 errors, 0 skips

問題ないね。

次はボタン動くかどうか見てみよう。

サーバーを起動して

$ rails server -b $IP -p $PORT

2番目のユーザーのページにアクセス。
フォローしてないのでフォローボタンが表示されてる。

f:id:kt_zoe:20170906123958p:plain

フォローボタン押すとfollowersの数が増えてフォロー解除ボタンに!上手く動いてそう。

f:id:kt_zoe:20170906124012p:plain

followingページ見てみると、ちゃんと追加されてる。

f:id:kt_zoe:20170906124111p:plain

今日の学習時間は【21分】

次は「12.2.5 [フォローする] ボタン (Ajax)」から。