LoginSignup
13
9

More than 3 years have passed since last update.

Ruby on Rails チュートリアル 第14章 データモデルの関連付け(フォロー フォロー解除)フィードの実装など 演習 解答

Last updated at Posted at 2019-02-08

近況報告

エンジニア転職成功しました。YouTubeもはじめました。

前回の続き

著者略歴
著者:YUUKI
ポートフォリオサイト:Pooks
現在:RailsTutorial2周目

第14章 ユーザーをフォローする 難易度 ★★★★ 5時間

挫折しないRailsチュートリアルの進め方を先にお読みください↓↓

Railsチュートリアルで挫折しない3つのポイント

この章では、他のユーザーをフォローしたり、フォロー解除したりするソーシャル的な仕組みと、
フォローしているユーザーの投稿をステータスフィード(いわゆるタイムライン)
に表示する仕組みを追加する。

そのために、まずはユーザー間の関係性をどうモデリングするかについて学ぶ。

その後、モデリング結果に対応するWebインターフェースを実装していく。

Webインターフェースの例としてAjaxについても後に詳解する。

最後に、ステータスフィードの完成版を実装する。

この最終章では、本書の中で最も難易度の高い手法をいくつか使っている。
その中には、ステータスフィードの作成のためにRuby/SQLを騙すテクニックも含まれる。

この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。

ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ。

この章で学ぶことは今まで最も難易度が高いため、
コードを書く前に一旦インターフェースの流れを理解する。

モックアップはこちら

image.png

出典:図 14.1: 現在のプロフィールページ

image.png

出典:図 14.2: フォローする相手を見つける

image.png

出典:図 14.3: ユーザーのプロフィール画面に [Follow] ボタンが表示されている

image.png

出典:図 14.4: プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた

image.png

出典:図 14.5: Homeページにステータスフィードが表示され、フォローのカウントが1増えた

ページ操作の全体的なフローは次の通りとなる。

①あるユーザーは(John Calvin)は自分のプロフィールページを最初に表示する
②フォローするユーザーを選択するためにUsersページに移動する
③Calvinは2番目のThomas Hobbesを表示し、Followボタンを押してフォローする
④Homeページに戻ると、followingカウントが1人増える
⑤Hobbesのマイクロポストがステータスフィードに表示されるになる

14.1 Relationshipモデル

ユーザーをフォローする機能を実装する第一歩は、データモデルを構成する。

今回のデータモデルは単純ではなく、
has_many(1対多)の関連付けを用いて
「1人のユーザーが複数のユーザーをhas_manyとしてフォローし、1人のユーザーに複数のフォロワーがいることをhas_manyで表す」
といった方法でも実装できる。

しかし、この方法ではたちまち壁に突き当たってしまう。

これを解決する為のhas_many_throughについても解説する。

Gitユーザーはこれまで同様新しいトピックブランチを作成する

$ git checkout -b following-users

14.1.1 データモデルの問題(および解決策)

ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみる。

あるユーザーが、別のユーザーをフォローしているところを考えてみる。

- CavinはHobbesをフォローしている。
- 逆から見ればHobbesはCalvinからフォローされている。
- CalvinはHobbesから見ればフォロワーであり、Calvinがhobbesをフォローしたことになる。
- Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合(フォローされてる人数)はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになる。
- しかし、これを逆で考えた場合(フォローしている人数)、英語の文法的`followeds`となり、英語の文法からも外れてしまう
- そこで、Railsではフォロー人数を`following`という呼称を採用している。
- したがって、あるユーザーがフォローしている全てのユーザーの集合は`calvin.following`となる

つまり、followersがフォロワー人数で、followingがフォロー人数を表すデータの表となる。

まずはfollowingテーブル(フォロー人数)を見ていく。

followingテーブルとhas_many関連付けを使って、フオローしているユーザーのモデリングができる。
user.followingはユーザーの集合でなければならないため、followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければならない。

さらに、それぞれの行はユーザーなので、これらのユーザーに名前(name)やパスワード(password)などの属性を追加する。

image.png

出典:図 14.6: フォローしているユーザーの素朴な実装例

上記のデータモデルの問題点は非常に無駄が多いこと。

各行には、フォローしているユーザーのidのみならず、名前やメールアドレスまである。

これらはいずれもusersテーブルに既にあるものばかり。

さらによくないことに、followersの方をモデリングする時にも、
同じくらい無駄の多いfollowersテーブルを別に作成しなければならなくなってしまう。

結論としては、このデータモデルはメンテナンスの観点から見て悪夢。
というのも、ユーザー名を変更するたびに、usersテーブルのそのレコードだけでなく、followingテーブルとfollowersテーブルの両方について、そのユーザーを含む全ての行を更新しなければならなくなる。

この問題の根本は、必要な抽象化を行なっていないことである。
正しいモデルを見つけ出す方法の1つは、
Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えることにある。

7章において、RESTアーキテクチャは、作成されたり削除されたりするリソースに関連していたことを思い出してみる。

ここで2つの疑問点が挙げられる。

①あるユーザーが別のユーザーをフォローする時、何が作成されるか?
②あるユーザーが別のユーザーをフォロー解除する時、何が削除されるか?

この点を踏まえて考えると、この場合アプリケーションによって作成または削除されるのは
2人のユーザーの関係(リレーションシップ)であることがわかる。

つまり、1人のユーザーは1対多の関係を持つことができ、さらにユーザーはリレーションシップを経由して多くのfollowing(またはfollowers)と関係を持つことができるということ。

このデータモデルには他にも解決しなくてはいけない問題がある。
Facebookのような友好関係(Friendships)では、本質的に左右対称のデータモデルが成り立つが、
Twitterのようなフォロー関係では左右非対称の性質がある。

すなわち、CalvinはHobbesをフォローしていても、HobbesはCalvinをフォローしていないといった関係性が成り立つ。

このような左右非対称な関係性を見分けるために、それぞれを
能動的関係(Active Relationship)と
受動的関係(Passive Relationship)と呼ぶことにする。

例えば先ほどの事例のような、CalvinがHobbesをフォローしているが、hobbesはCalvinをフォローしていない場合では、CalvinはHobbesに対して能動的関係を持っていることになる。

逆に、HobbesはCalvinに対して受動的関係を持っていることになる。

まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていく。
(受動的関係についてはのちに考える)

先ほどのfollowingデータモデルは実装のヒントにして考える。

フォローしているユーザーはfollowed_idがあれば識別することができるので、先ほどのfollowingテーブルをactive_relationships(能動的関係)テーブルと見立ててみる。

ただし、ユーザー情報は無駄なので、ユーザーid以外の情報は削除する。
そして、followed_idを通して、usersテーブルのフォローされているユーザーを見つけるようにする。

このデータモデルを模式図にすると、以下のようになる。

image.png

出典:図 14.7: 能動的関係をとおしてフォローしているユーザーを取得する模式図

間にactive_relationshipsを挟むことで、フォローとフォロワーの関係性がスムーズに繋がっている。

能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになる。

したがって、テーブル名にはこの「関係」を表す「relationships」を使う。

モデル名はRailsの慣習にならって、Relationshipとする。
作成したRelationshipデータモデルを以下に示す。

1つのrelationshipsテーブルを使って2つのモデル(能動的関係と受動的関係)をシミュレートする方法については後に説明する。

image.png

出典:図 14.8: Relationshipデータモデル

このデータモデルを実装するために、まずは上記のデータモデルに対応したマイグレーションを生成する。

$ rails g model Relationship follower_id:integer followed_id:integer

このリレーションシップは今後follower_idfollowed_idで頻繁に検索することになるので、
それぞれのカラムインデックスを追加する。

[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true        # 複合キーインデックスにし、お互いがユニークであることを保証
  end
end

複合キーインデックスで、follower_idfollowed_idの組み合わせが必ずユニークであることを保証する仕組みを作っている。

これにより、あるユーザーが同じユーザーを2回以上フォローすることを防いでいる。

もちろん、このような重複が起きないよう、インタフェース側の実装でも注意を払う。

しかし、ユーザーが何らかの方法で(例えばcurlなどのコマンドラインツール)Relationshipのデータを操作するようなことも起こり得る。

そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができる。

relationshipsテーブルを作成するために、いつものようにデータベースのマイグレーションを行う。

$ rails db:migrate

演習

1:id=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるか?

引数で受け取ったid=1にフォローされているユーザー(id: 2,7,10,8)のidをそれぞれ1つずつ返す

>> user.following.map(id:1)
2
7
10
8 

2:id=2のユーザーに対してuser.followingを実行すると、結果はどうなるか?
また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるか?

>> user.following(id:2)
=> [id:1,name:Michael Hartl,email:mhartl@example.com]
>> user.following.map(id:2)
=> 1

14.1.2 User/Relationshipの関連付け

フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行う。

1人のユーザーにはhas_many(1対多)のリレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属している。(belongs_to)

マイクロポスト作成の時と同様、下記のようなユーザー関連付けのコードを使って新しいリレーションシップを作成する。

user.active_relationships.build(followed_id: ...) #user.active_relationshipsをデータモデルとして引数で受け取った値と関連付けて、カラムを生成する

この時点では、User/Micropostの関連付けのモデルのようにはならない。

1つ目の違いとして、以前、ユーザーとマイクロポストの関連付けをした時は

class User < ApplicationRecord
  has_many :microposts

end

このように書いた。

引数の:micropostsシンボルから、Railsはこれに対応するMicropostモデルを探し出し、見つけることができた。

しかし、今回のケースで同じように書くと

has_many :active_relationships

となってしまい、ActiveRelationshipモデルを探してしまうので、
相互にフォローユーザーを繋ぐRelationshipモデルを見つけることができない。

このため、今回のケースではRailsに探して欲しいモデルのクラス名を明示的に伝える必要がある。

2つ目の違いは、先ほどの逆のケースについて。

以前はMicropostモデルで

class Micropost < ApplicationRecord
  belongs_to :user


end

このように書いた。

micropostsテーブルにはuser_id属性があるので、
これを辿って対応するユーザーを特定できた。

DBの2つのテーブルを繋ぐとき、このようなidは外部キー(foreign key)と呼ぶ。

すなわち、Userモデルに繋げる外部キーが、Micropostモデルのuser_id属性ということ。

この外部キーの名前を使って、Railsは関連付けの推測をしている。

具体的には、Railsはデフォルトでは外部キーの名前を_idといったパターンとして理解し、
に当たる部分からクラス名(正確には小文字に変換されたクラス名)を推測する。

class Micropost < ApplicationRecord
  belongs_to :user     #Micropostはmicropostモデルのuser_id属性が外部キーと自動で推測する)

ただし、マイクロポストではユーザーを例として扱ったが、
今回のケースでは
フォローしているユーザーをfollower_idという外部キーを持って特定しなくてはならない

また、followerというクラス名は存在しないので、ここでもRailsに正しいクラス名を伝える必要が発生する。

先ほどの説明をコードにまとめると、UserとRelationshipの関連付けは以下のようになる。

user.rb
class User < ApplicationRecord
  # 関連付け
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:     "Relationship",
                                  foreign_key:    "follower_id",
                                  dependent:      :destroy

明示的にclass名や外部キー、destroyも追加している。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があるため)

relationship.rb
class Relationship < ApplicationRecord
  # 1対1の関連付け
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

なお、followerの関連付けはまだ使わない。
(書いておくことで構造の理解の手助けになる)

user.rbrelationship.rbで定義した関連付けにより、13.1で以前紹介したような多くのメソッドが使えるようになった。

出典:表 14.1: ユーザーと能動的関係の関連付けによって使えるようになったメソッドのまとめ

これらのメソッドを使えば、フォロワーを返したり、フォローしているユーザーを返したりできる。

演習

1:コンソールを開き、上記表のcreateメソッドを使って、ActiveRelationshipを作ってみる。
DB上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみる。

>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$ufvz2x2ljsYgknbfQaQrNOF5uG5PP.1YP2jIXom1qCU...", remember_digest: nil, admin: true, activation_digest: "$2a$10$YtXwZx1hETpK66tpv23VJO7a47hav3sFWdwHIpfQDLy...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil>
>> user_second = User.second
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Jordyn Heaney", email: "example-1@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$3IGSHyPf/ofme0Fump8NF.4kP13rVb9UmiSRnNJkiLt...", remember_digest: nil, admin: false, activation_digest: "$2a$10$vJENgJrXHkTu3.d8/Bpz8OKUK.AJUW1objabSVuoWHE...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil>

>> user.active_relationships.create(followed_id: user_second.id)
   (0.1ms)  begin transaction
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (2.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-04 18:18:22.015668"], ["updated_at", "2019-02-04 18:18:22.015668"]]
   (6.2ms)  commit transaction
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">

2:active_relationships.followerとactive_relationships.followedの値がそれぞれ正しいことを確認。

フォローしてるのが1で、フォローされてるのが2だと確認できる。

=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">

14.1.3 Relationshipのバリデーション

ここでRelationshipモデルの検証を追加して完全なものにしておく。

テストコードとアプリケーションコードを作って実装していく。

ただし、User用のfixtureファイルと同じように、生成されたRelationship用のfixtureでは、
マイグレーションで制約させた一意性を満たすことができない。

このままだと正しくテストを行えないので、今の時点では、
生成されたRelationship用のfixtureファイルを空にしておく。

fixtures/relationships.yml
# 空にする

早速、簡単なテストとバリデーションを記入する。

relationship_test.rb
require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase

    def setup
      @relationship = Relationship.new(follower_id: users(:michael).id,
                                       followed_id: users(:archer).id )
    end

    test "should be valid" do
      assert @relationship.valid?
    end

    test "should require a follwer_id" do
      @relationship.follower_id = nil
      assert_not @relationship.valid?
    end

    test "should require a followed_id" do
      @relationship.followed_id = nil
      assert_not @relationship.valid?
    end
end
relationship.rb
class Relationship < ApplicationRecord
  # 1対1の関連付け
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

これでテストはパスする。

$ rails t

演習

1:Relationshipモデルのvalidatesをコメントアウトしてもテストが成功することを確認。

relationship.rb
  # validates :follower_id, presence: true
  # validates :followed_id, presence: true
end

$ rails t
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips

テストが成功する理由は、Rails5だと初期の時点でバリデーションが掛かってるから。

14.1.4 フォローしているユーザー

Relationshipの関連付けの核心followingfollowersに取りかかる。

今回はhas_many throughを使う。

1人のユーザーにはいくつもの「フォローする」「フォローされる」といった関係性がある。
この関係性を多対多と呼ぶ。

デフォルトのhas_many throughという関連付けでは、
Railsはモデル名(単数形)に対応する外部キーを探す。

has_many :followeds, through: :active_relationships

Railsはfollowedsというシンボルを見て、
これをfollowed単数形に変え、
relationshipsテーブルのfollowed_idを使って対象のユーザーを取得してくる。

しかし、
user.followedsという使い方は英語としては不適切。

代わりに、user.followingという名前を使う。

そのためには、Railsのデフォルトを上書きする必要がある。
ここでは:sourceパラメーターを使って、following配列の元はfollowed idの集合である
ということを明示的にRailsに伝える。

user.rb
class User < ApplicationRecord
  # 関連付け
  has_many :microposts, dependent: :destroy
  # 1対多の関連付け
  has_many :active_relationships, class_name:     "Relationship",
                                  foreign_key:    "follower_id",
                                  dependent:      :destroy
  has_many :following,  through: :active_relationships, source: :followed

上記で定義した関連付けにより、フォローしているユーザーを配列の様に扱える様になった。

例えば、include?メソッドを使ってフォローしているユーザーの集合を調べてみたり、
関連付けを通してオブジェクトを探しだせるようになる。

user.following.include?(other_user)
user.following.find(other_user)

followingで取得したオブジェクトは、配列の様に要素を追加したり削除したりすることができる。

user.following << other_user
user.following.delete(other_user)

<<演算子で配列の最後に追記することができる。

followingメソッドで配列の様に扱えるだけでも便利だが、
Railsは単純な配列ではなく、もっと賢くこの集合を扱っている。例えば次のようなコードでは

following.include?(other_user)

フォローしている全てのユーザーをDBから取得し、その集合に対してinclude?メソッドを時実行しているように見えるが、実際はDBの中で直接比較をするように配慮している。(other_userがいるかどうかの比較を行なっている)

なお、次のようなコードでは

user.microposts.count

DBの中で合計を計算した方が高速になる点に注意する。

次に、followingで取得した場合をより簡単に取り扱うために、
followunfollowといった便利メソッドを追加する。

これらのメソッドは、例えばuser.follow(other_user)といった具合に使う。

さらに、これに関連するfollowing?論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにする。

今回は、こういったメソッドはテストから先に書いていく。
と言うのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから。

一方で、Usermモデルに対するテストは書くのは簡単かつ今すぐできるので、
先に書いていく。

具体的には、

  • following?メソッドであるユーザーをまだフォロしていないことを確認
  • followメソッドを使ってそのユーザーをフォローできたことを確認
  • unfollowメソッドでフォロー解除できたことを確認

といった具合でテストしていく。

user_test.rb
  test "should follow and unfollow a user" do
    michael     = users(:michael)
    archer      = users(:archer)
    assert_not  michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end

followingによる関連付けを使ってfollowunfollowfollowing?メソッドを実装していく。

このとき、可能な限りselfを省略している点に注目。

user.rb
  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end

  # ユーザーをフォロー解除する
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしてたらtrueを返す
  def following?(other_user)
    following.include?(other_user)
  end

private

上記コードを追加することで、テストはパスする。

13 tests, 19 assertions, 0 failures, 0 errors, 0 skips

演習

1:コンソールを開き、user_test.rbのコードを順々に実行してみる。

>> michael = User.find(3)
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
=> #<User id: 3, name: "Brittany Schiller", email: "example-2@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$/KeYm3kd5PfnTaPWl.o/q.yf4I.Q5iXW7K3oSqywWb0...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QctyRqieo7GHcwqke8DSZOm/bbSlBeJ/66VLUF6eukO...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil>
>> archer = User.find(4)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
=> #<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil>
>> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 3], ["id", 4], ["LIMIT", 1]]
=> false
>> michael.follow(archer)
   (0.1ms)  begin transaction
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  SQL (5.7ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 3], ["followed_id", 4], ["created_at", "2019-02-05 16:36:58.158969"], ["updated_at", "2019-02-05 16:36:58.158969"]]
   (9.8ms)  commit transaction
  User Load (0.2ms)  SELECT  "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ?  [["follower_id", 3], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil>]>
>> michael.following?(archer)
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 3], ["id", 4], ["LIMIT", 1]]
=> true
>> michael.unfollow(archer)
  Relationship Load (0.3ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 3], ["followed_id", 4], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (3.3ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 2]]
   (10.6ms)  commit transaction
=> #<Relationship id: 2, follower_id: 3, followed_id: 4, created_at: "2019-02-05 16:36:58", updated_at: "2019-02-05 16:36:58">
>> michael.following?(archer)
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ?  [["follower_id", 3], ["id", 4], ["LIMIT", 1]]
=> false

2:先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみる。

上記で確認できる。

14.1.5 フォロワー

リレーションシップにuser.followersメソッドを追加する。

user.followingはフォローしている人数
user.followersはフォローされてる人数(フォロワー)である。

フォロワーの配列を展開するために必要な情報は、relationshipsテーブルに既にあり、
active_relationshipsテーブルを再利用することで出来る。

実際、follower_idfollowed_idを入れ替えるだけで、
フォロワーについてもフォローする場合と全く同じ活用が出来る。

データモデルは以下。

image.png

出典:図 14.9: Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル

要は、active_relationshipspassive_relationshipsに入れ替えて、
followed_idfollower_idを入れ替えるだけ。

上記のデータモデルの実装をuser.rbにhas_manyを使って行う。

user.rb
class User < ApplicationRecord
  # 関連付け
  has_many :microposts, dependent: :destroy
  # 1対多の関連付け
  has_many :active_relationships, class_name:     "Relationship",
                                  foreign_key:    "follower_id",
                                  dependent:      :destroy
  has_many :passive_relationships, class_name:    "Relationship",
                                   foreign_key:   "followed_id",
                                   dependent:     :destroy
  # 多対多の関連付け
  has_many :following,  through: :active_relationships,   source: :followed
  has_many :followers,  through: :passive_relationships,  source: :follower

一点、上記で注意すべき箇所は次の様に参照先(followers)を指定するための
:sourceキーを省略してもよかった点。

has_many :followers, through: :passive_relationships

これは、:followers属性の場合、
Railsがfollowersを単数形にして自動的に外部キーfollower_idを探してくれるから。

ただ、必要がないがhas_many :followingとの類似性を強調させるために書いている。

次に、followers.include?メソッドを使って先ほどのデータモデルをテストしていく。

テストコードは以下の通り。ちなみにfollowing?と対照的なfollowed_by?メソッドを定義してもよかったが、
サンプルアプリケーションで実際に使う場面がなかったのでこのメソッドは省略している。

user_test.rb
  test "should follow and unfollow a user" do
    michael     = users(:michael)
    archer      = users(:archer)
    assert_not  michael.following?(archer)
    michael.follow(archer)
    assert michael.following?(archer)
    assert archer.followers.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end

↑archerのフォロワーにmichaelは含まれているかどうかテストしている。

上記のテストは実際には多くの処理が正しく動いていなければパスしない。
つまり、受動的関係に対するテストは実装の影響を受けやすい。

この時点で、全てのテストはパスする。

13 tests, 20 assertions, 0 failures, 0 errors, 0 skips

演習

1:コンソールで、何人かのユーザーが最初のユーザーをフォローしている状況作ってみる。
最初のユーザーをuserとすると、user.followers.map(&:id)のidの値はどのようになっているか?

>> user = User.first
>> user_second = User.second
>> user.followers.map(&:id)
" = ?  [["followed_id", 1]]
=> [2, 3]

2:user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認。

>> user.followers.count
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 2

3:user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているか?
また、user.followers.to_a.countの実行結果と違っている箇所はあるか?
100万人ユーザーがフォロワーにいた場合はどうなるか?

   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
>> user.followers.to_a.count
=> 2

フォロワーが100万人いたらそのまま100万と言う数値が返されるが配列を生成する為、時間が掛かるしDBにも負担が掛かる。

14.2 FollowのWebインターフェイス

これまでやや複雑なデータモデリングの技術を駆使して実装した。

次は、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装する。
また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する。

後に、ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させる。

14.2.1 フォローのサンプルデータ

前章と同じように、サンプルデータを自動生成するrails db:seedを使って、DBにサンプルデータを登録できるとやや便利。

先にサンプルデータを自動生成出来るようにしておけば、Webページの見た目のデザインから先に取り掛かることができ、バックエンド機能の実装を後に回すことが出来る。

リレーションシップのサンプルデータを生成するためのコードをseedに書いていく。
ここでは、最初のユーザーにユーザー3からユーザー51をフォローさせ、それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせる。

seed.rb
$ rails db:migrate:reset
$ rails db:seed

演習

1:コンソールを開き、User.first.followers.countの結果がリスト14.14で期待している結果と合致していることを確認。

>> User.first.followers.count
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 38

2:User.first.following.countの結果も合致していることを確認。

>> User.first.following.count
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> 49

14.2.2 統計と[Follow]フォーム

これでサンプルユーザに、フォローしているユーザーとフォロワーができました。
プロフィールページとHomeページを更新して、これを反映する。

最初に、プロフィールページとHomeページに、
フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。

次に、フォローしているユーザーの一覧(following)と
フォロワーの一覧(followers)を表示する専用のページを作成する。

Twitterの慣習にしたがってフォロー数の単位にはfollowingを使い、
例えば50 followingといった具合に表示する。

image.png

出典:図 14.10: 統計情報パーシャルのモックアップ

上記の統計情報には、現在のユーザーがフォローしている人数と、
現在のフォロワーの人数が表示されている。

それぞれの表示はリンクになっており、専用の表示ページに移動できる。

これらのリンクはダミーテキスト#を使って無効にしていた。
しかし、ルーティングについての知識もだいぶ増えてきたので、今回は実装することにする。

実際のページ作成は後にルーティングは今実装する。

このコードでは、resourcesブロックの内側で:memberメソッドが使っている。
これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。

routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get     '/help',    to: 'static_pages#help'
  get     '/about',   to: 'static_pages#about'
  get     '/contact', to: 'static_pages#contact'
  get     '/signup',  to: 'users#new'
  post    '/signup',  to: 'users#create'
  get     '/login',   to: 'sessions#new'
  post    '/login',   to: 'sessions#create'
  delete  '/logout',  to: 'sessions#destroy'
  resources :users do                                                           # usersリソースをRESTfullな構造にするためのコード。
    member do
      get :following, :followers
    end
  end

  resources :account_activations, only: [:edit]                                 # editアクションのみaccount_activationsリソースを適用
  resources :password_resets,     only: [:new, :create, :edit, :update]         # password再設定用のリソースを適用
  resources :microposts,          only: [:create, :destroy]                     # micropostsリソースをcreateとdestroyアクションにのみ適用
end

この場合のURLは/users/1/following/users/1/followers
のようになるのではないかと推測。

また、どちらもデータを表示するページなので、適切なHTTPメソッドはGETリクエストになる。

したがって、getメソッドを使って適切なレスポンスを返す。

ちなみに、memberメソッドを使うとユーザーidが含まれているURLを扱うようになる。

idを指定せずに全てのメンバーを表示するには、次のようにcollectionメソッドを使う。

resources :users do
  collection do
    get :tigers
  end
end

このコードは/users/tiggersというURLに応答する。
生成されるルーティングテーブルは以下。

この表で示したフォロー用とフォロワー用の名前付きルートを、今後の実装で使う。

HTTPリクエスト URL アクション 名前付きルート
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)

出典:表 14.2: カスタムルールで提供するリスト 14.15のRESTfulルート

ルーティングを定義したので、統計情報のパーシャルを実装する準備が整った。
このパーシャルでは、divタグの中に2つのリンクを含めるようにする。

_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

このパーシャルはプロフィールページとHomeページの両方に表示されるので、
最初の行では、次のコードで現在のユーザーを取得する。

<% @user ||= current_user %>

@userがnilでない場合(つまりプロフィールページ)は何もせず、
nilの場合には@userにcurrent_userに代入するコードである。

その後、フォローしているユーザーの人数を、次のように関連付けを使って計算する。

@user.following.count

これはフォロワーについても同様。

@user.microposts.count

なお、今回も以前と同様に、Railsは高速化のためにDB内で合計を計算している点に注意。

一部の要素で、次のようにCSS idを指定していることにも注目。

<strong id="following" class="stat">
</strong>

こうしておくと、Ajaxを実装するときに便利です。
そこでは、一意のidを指定してページ要素にアクセスしている。

これで統計情報パーシャルが出来上がる。Homeページにこの統計情報を表示するには、以下のようにすると良い。

home.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

統計情報にスタイルを与えるために、SCSSを追加する。

変更の結果、Homeページは以下のようにする。

custom.scss
/* sidebar */

.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid &gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    $:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

image.png

出典:図 14.11: Homeページにフォロー関連の統計情報を表示する

この後すぐ、プロフィールにも統計情報パーシャルを表示するが、
今のうちに[Follow]/[Unfollow]ボタン用のパーシャルを作成する。

_follow_form.html.erb
<!--現在のユーザーがURLのユーザーとは違う場合-->
<% unless current_user?(@user) %>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render 'unfollow' %>
    <% else %>
      <%= render 'follow' %>
    <% end %>
  </div>
<% end %>

このコードは、followとunfollowのパーシャルに作業を振っているだけ。
(urlのユーザーをログインユーザーがフォローしていればunfollow,フォローしていなければfollowをレンダリング)

パーシャルでは、Relationshipsリソース用の新しいルーティングが必要。
これを、Micropostsリソースの例に従って作成する。

routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get     '/help',    to: 'static_pages#help'
  get     '/about',   to: 'static_pages#about'
  get     '/contact', to: 'static_pages#contact'
  get     '/signup',  to: 'users#new'
  post    '/signup',  to: 'users#create'
  get     '/login',   to: 'sessions#new'
  post    '/login',   to: 'sessions#create'
  delete  '/logout',  to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :users                                                              # usersリソースをRESTfullな構造にするためのコード。
  resources :account_activations, only: [:edit]                                 # editアクションのみaccount_activationsリソースを適用
  resources :password_resets,     only: [:new, :create, :edit, :update]         # password再設定用のリソースを適用
  resources :microposts,          only: [:create, :destroy]                     # micropostsリソースをcreateとdestroyアクションにのみ適用
  resources :relationships,       only: [:create, :destroy]                     # 
end

フォロー/フォロー解除用のパーシャルも書く。

_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
    html: { method: :delete }) do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
<% end %>

これら2つのフォームでは、いずれもform_forを使ってRelationshipモデルオブジェクトを操作している。

これらの2つのフォームの主な違いは、フォローフォームでは新しいリレーションシップを作成するのに対し、
アンフォローフォームでは既存のリレーションシップを見つけ出すという点。

すなわち、前者はPOSTリクエストをRelationshipsコントローラに送信してリレーションシップをcreate(作成)し、
後者はDELETEリクエストを送信してリレーションシップをdestroy(削除)するということ。

最終的に、このフォロー/アンフォローフォームにはボタンしかないことが理解できる。

しかし、それでもフォローフォームではfollowed_idをコントローラに送信する必要がある。
これを行うために、hidden_field_tagメソッドを使う。

このメソッドは、次のフォーム用HTMLを生成する。

<input id="followed_id" name="followed_id" type="hidden" value="3" />

12章で見たように、隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができる。

これでパーシャルとしてフォロー用フォームをプロフィールページに表示できるようになった。

showビューに表示用のhtmlを書く。

show.html.erb
        </section>
        <section class="stats">
            <%= render 'shared/stats' %>
        </section>
    </aside>
    <div class="col-md-8">
        <%= render 'follow_form' if logged_in? %>
        <% if @user.microposts.any? %>

プロフィールには、それぞれ[Follow][Unfollow]ボタンが表示される。

スクリーンショット 2019-02-07 15.56.13.png

これらのボタンを実装するには、二通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。

でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。

演習

1:ブラウザから/users/2にアクセスし、フォローボタンが表示されていることを確認する。
同様に、/users/5ではUnfollow]ボタンが表示されているはず。

さて、/users/1にアクセスすると、どのような結果が表示されるか?

users/1はログインユーザーなのでボタンが消える。

スクリーンショット 2019-02-07 16.08.47.png

2:ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認。

確認済み。

3:Homeページに表示されている統計情報に対してテストを書いてみる。
同様にして、プロフィールページにもテストを追加してみる。

site_layout_test.rb
  test "count relationships" do
    log_in_as(@user)
    get root_path
    assert_match @user.active_relationships.count.to_s, response.body
    assert_match @user.passive_relationships.count.to_s, response.body
  end
users_profile_test.rb
    end
    assert_select @user.microposts.count
    assert_match @user.active_relationships.to_s, response.body
    assert_match @user.passive_relationships.to_s, response.body
  end

テストがパスしたのでOK。

14.2.3 [Following][Followers]ページ

フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。

どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。

さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。

image.png

出典:図 14.14: フォローしているユーザー用ページのモックアップ

image.png

出典:図 14.15: ユーザーのフォロワー用ページのモックアップ

ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。

Twitterに倣って、どちらのページでもユーザーのログインを要求するようにする。

そこで前回のアクセス制御と同様に、まずはテストから書いていく。

今回使うテストは以下の通り。

上記コードではfollowing/followersの名前付きルートを使っている点に注意。

users_controller_test.rb
  test "should redirect following when not logged in" do
    get following_user_path(@user)
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get followers_user_path(@user)
    assert_redirected_to login_url
  end

この実装には1つだけトリッキーな部分がある。
それはUsersコントローラに2つの新しいアクションを追加する必要があるということ。

これはroutesで定義した2つのルーティングに基づいており、これらはそれぞれfollowingおよびfollowersと呼ぶ必要がある。

それぞれのアクションでは、タイトルを認定し、ユーザーを検索し、@user.followingまたは@user.followersからデータを取り出し、ページネーションを行なって、ページを出力する必要がある。

users_controller.rb
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

これまで見てきたように、
Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出す。

例えば、showアクションの最後でshow.html.erbを呼び出す、といった具合。

一方で、上記のいずれのアクションもrenderを明示的に呼び出し、show_followという同じビューを出力している。
したがって、作成が必要なビューはこれ1つ。

renderで呼び出しているビューが同じである理由は、このERBはどちらの場合でもほぼ同じであり、
1つのファイルで両方の場合をカバーできるから。

show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

users_controllerでは、

  • followingアクションでfollowingを通してshow_followビューを呼び出し、
  • followersアクションではfollowersを通してshow_followビューを呼び出す。

この時、上記コードでは現在のユーザーを一切使っていないので、
他のユーザーのフォロワー一覧ページもうまく動く。

スクリーンショット 2019-02-07 21.00.33.png

スクリーンショット 2019-02-07 21.01.00.png

スクリーンショット 2019-02-07 21.01.47.png

beforeフィルターを既に実装しているため、テストはパスする。

12 tests, 21 assertions, 0 failures, 0 errors, 0 skips

次に、show_followの描画結果を確認するため、統合テストを書いていく。
ただし、今回は基本的なテストだけに留めておき、網羅的なテストにはしない。

これはHTML構造を網羅的にチェックするテストは壊れやすく、生産性を落としかねないから。

したがって今回は、
正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書く。

いつものように統合テストを生成するところから始める。

$ rails g integration_test following
Running via Spring preloader in process 8224
      invoke  test_unit
      create    test/integration/following_test.rb

次に、テストデータをいくつか揃える。
リレーションシップ用のfixtureにデータを追加する。

次のように書くことで

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

ユーザーとマイクロポストは関連付けできる。

ユーザー名を書かずに

user: michael

ではなく

user_id: 1

このようなユーザーidを指定しても関連付けできる。
この例を参考に、Relationship用のfixtureにテストデータを追加する。

relationships.yml
one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

上記のfixtureでは、

前半の2つでMichaelがLanaとMaloryをフォローし、
後半の2つでLanaとArcherがMichaelをフォローしている。

あとは、正しい数かどうかを確認するために、
assert_matchメソッドを使ってプロフィール画面のマイクロポスト数をテストする。

さらに、正しいURLかどうかをテストするコードも加えると、以下のようになる。

following_test.rb
  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

上記では

assert_not @user.following.empty?

このようなコードを書いているが、これは次のコードを確かめる為のテスト。

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

つまり、@user.followingの結果がtrueであれば、上記のブロックが実行できなくなる為、
その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいる。

上の変更を加えるとテストが成功する筈。

66 tests, 324 assertions, 0 failures, 0 errors, 0 skips

演習

1:ブラウザから/users/1/followers/users/1/followingを開き、それぞれが適切に表示されていることを確認。
サイドバーにある画像は、リンクとしてうまく機能しているか?

スクリーンショット 2019-02-07 22.27.08.png

スクリーンショット 2019-02-07 22.27.58.png

スクリーンショット 2019-02-07 22.28.16.png

OK

2:following_testassert_select関連のコードをコメントアウトしてみて、正しくテストが失敗することを確認。

show_html.erb
          <% @users.each do |user| %>
            <%= #link_to gravatar_for(user, size: 30), user %>
          <% end %>
$ rails t
            app/controllers/users_controller.rb:61:in `followers'
            test/integration/following_test.rb:20:in `block in <class:FollowingTest>'

 app/controllers/users_controller.rb:54:in `following'
            test/integration/following_test.rb:11:in `block in <class:FollowingTest>'

  66/66: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.39352s
66 tests, 318 assertions, 0 failures, 2 errors, 0 skips

12.2.4 [Follow]ボタン(基本編)

ビューが整ってきた。
いよいよ[Follow]/[Unfollow]ボタンを動作させる。

フォローとフォロー解除はそれぞれリレーション湿布の作成と削除に対応しているため、まずはRelationshipsコントローラが必要。
いつものようにコントローラを生成させる。

$ rails g controller Relationships

Relationshipsコントローラのアクションでアクセス制御することはそこまで難しくない。

しかし、前回のアクセス制御の時と同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていく。
今回はまず、コントローラのアクションにアクセスする時、ログイン済みのユーザーであるかどうかをチェックする。

もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認する。

relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionDispatch::IntegrationTest

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

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

relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

[Follow]/[Unfollow]ボタンを動作させるためには、フォームから送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要がある。

その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッド(Userモデルで定義した)を使う。

relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  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
end

上記をみてれば、先ほどのセキュリティ問題が実はそれほど重要なものではないことを理解できる。

もし、ログインしていないユーザーが(curlなどのコマンドラインツールなどを使って)これらのアクションに直接アクセスするようなことがあれば、current_usernilになり、
どちらのメソッドでも2行目で例外が発生する。

エラーにはなるが、アプリケーションやデータに影響は生じない。

このままでも支障はないが、このような例外には頼らない方がいいので、セキュリティの為のレイヤーを追加した。

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

どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。
(振る舞いを検証する統合テストはのちに実装する)

スクリーンショット 2019-02-08 4.49.01.png

フォローしていないユーザーの画面

スクリーンショット 2019-02-08 4.49.26.png

ユーザーをフォローした結果

演習

1:ブラウザ上から/users/2のFollow/Unfollow実行して動いているか確認

確認済み。

2:先ほどの演習を終えたら、Railsサーバーのログを見てみる。
フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているか?

Started GET "/users/2" for 122.50.45.13 at 2019-02-07 19:55:41 +0000
Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/show.html.erb within layouts/application

フォローした場合、/users/2のビューが描画されている

Started GET "/users/2" for 122.50.45.13 at 2019-02-07 19:56:44 +0000
Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#show as HTML
  Parameters: {"id"=>"2"}
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Rendering users/show.html.erb within layouts/application

フォロー解除した場合、/users/2のビューが描画されている。

14.2.5 [Follow]ボタン(Ajax編)

フォロー関連の機能の実装は完了したが、ステータスフィードに取りかかる前にもう1つだけ機能を洗練させてみる。

先ほどはRelationshipsコントローラのcreateアクションdestroyアクションを単に元のプロフィールにリダイレクトしていた。
つまり、

①ユーザーはプロフィールページを最初に表示
②ユーザーをフォロー
③すぐ元のページにリダイレクト

という流れになる。

ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのか。
答えは否(ハンターハンター風)で、同じページにリダイレクトさせる必要はない。

この問題は、Ajaxを使えば解決できる。

Ajaxを使うことで、Webページからサーバーに「非同期」でページを遷移させることなくリクエストを送信することができる。

WebフォームにAjaxを採用するのは今や当たり前で、RailsでもAjaxを簡単に実装できるようになっている。

フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単。

例えば、次のコードがあるとすると

form_for

上のコードを次のように置き換えるだけ

form_for ..., remote: true

これだけでRailsは自動的にAjaxを使うようになる。

_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
    html: { method: :delete },
    remote: true) do |f| %>
    <%= f.submit "Unfollow", class: "btn" %>
<% end %>

ERbによって実際に生成されるHTMLはこちら

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post" >
</form>

ここでは、formタグの内部でdata-remote="true"を設定している。

これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのもの。
(現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっている)

フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、
Ajaxリクエストに応答できるようにする。

こういったリクエストの種類によって応答を場合分けする時は、respond_toメソッドを使う。

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

上記のブロック内のコードのうち、いずれかの1行が実行されるという点が重要。

このため、respond_toメソッドは、上から順に逐次処理(シリアル)というより、
:if文を使った分岐処理に近いイメージ*

RelationshipsコントローラでAjaxに対応させるために、respond_toメソッドをcreateアクションとdestroyアクションにそれぞれ追加してみる。

この時、ユーザーのローカル変数(user)を@userに変更している点に注目。

これは、_follow.html.erb_unfollow.html.erbを実装したことにより、
ビューで変数を使うインスタンス変数が必要になったからである。
(Ajaxによる)

relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  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
end

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

application.rb
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

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

一方で、JavaScriptが有効になっていても、まだ十分に対応できていない部分がある。

というのも、Ajaxリクエストを受診した場合は、Railsが自動的にアクションと同じ名前を持つ
JavaScript用の埋め込みRubyファイル(create.js.erb destroy.js.erb)などを呼び出す為、これらのファイルを作成する必要がある。

.js.erbでは、JSと埋め込みRubyをミックスして現在のページに対するアクションを実行することができる。

ユーザーをフォローしたときや、フォロー解除した時にプロフィールページを更新するために、
これらのファイルが使われる。

JS-ERbファイルの内部では、DOM(Document Object Model)を使ってページ操作するため、
RailsがjQuery JavaScriptヘルパーを自動的に提供している。

これによりjQueryライブラリの膨大なDOM操作用メソッドが使えるようになるが、
今回使うのはわずか2つ。

まず1つ目は、$とCSS idを使って、DOM要素にアクセスする文法について知る必要がある。
例えば、follow_formの要素をjQueryで操作するには、次のようにアクセスする。

$("#follow_form")

これはフォームを囲むdivタグであり、フォームそのものではない。

jQueryの文法はCSSの記法から影響を受けており、#シンボルを使ってCSSのidを指定する。

jQueryはCSSと同様、ドット.を使ってCSSクラスを操作できる。

次に必要なメソッドはhtml。
これは、引数の中で指定された要素の内側にあるHTMLを更新する。

例えば、フォロー用フォーム全体を"foobar"という文字列で置き換えたい場合は、次のようなコードになる。

$("#follow_form").html("foobar")

純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby(ERb)が使える。

create.js.erbファイルでは、フォロー用のフォームをunfollowパーシャルで更新し、フォロワーのカウントを更新するのにERbを使っている。

このコードではescape_javascriptメソッドを使っている点に注目。

このメソッドは、
JavaScriptファイル内にHTMLを挿入する時に実行結果をエスケープするために必要。

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

各行の末尾にセミコロン;があることに注目。
(これは1950年代中頃に開発されたALGOLまで遡るらしい)

destroy.js.erbファイルの方も同様です。

create.js.erb
$("#follow_form").html("<%= escape_javascript(render(`users/unfollow`)) %>");
$("#followers").html('<%= @user.followers.count %>');
destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render(`users/follow`)) %>");
$("#followers").html('<%= @user.followers.count %>');

これらのコードにより、プロフィールページを更新させずにフォローとフォロー解除できるようになった筈。

演習

1:ブラウザから/users/2にアクセスし、うまく動いているかどうか確認。

確認済み。

2:先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認。

   (0.0ms)  begin transaction
  CACHE User Load (0.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (2.2ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-08 00:24:36.494125"], ["updated_at", "2019-02-08 00:24:36.494125"]]
   (8.0ms)  commit transaction
  Rendering relationships/create.js.erb
  Relationship Load (0.1ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
  Rendered users/_unfollow.html.erb (2.0ms)

   (0.1ms)  begin transaction
  SQL (2.8ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 100]]
   (8.5ms)  commit transaction
  Rendering relationships/destroy.js.erb
  Rendered users/_follow.html.erb (1.2ms)

きちんとjs.erbファイルがレンダリングされている。

14.2.6 フォローをテストする

フォローボタンが動くようになったので、バグを検知する為のシンプルなテストを書いていく。

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

具体的なコードは

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }
end

これは標準的なフォローに対するテスト。
ただ、Ajax版もやり方はだいたい同じ。

Ajaxのテストでは、xhr :trueオプションを使うようにするだけ。

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }, xhr: true
end

ここで使っているxhr(XlHttpRequest)というオプションをtrueにすると
Ajaxでリクエストを発行するよに変わる。

したがって、respond_toでは、JavaScriptに対応した行が実行されるようになる。

また、ユーザーをフォロー解除する時も構造は殆ど同じで、postメソッドをdeleteメソッドに置き換えてテストする。

つまり、そのユーザーのidとリレーションシップのidを使ってDELETEリクエストを送信し、フォローしている数が1つ減ることを確認する。

したがって、実際に加えるテストは

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship)
end

上の従来通りのテストと、下のAjax用のテストの2つになる。

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship), xhr: true
end

これらのテストをまとめた結果

following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
    @other = users(:archer)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "should follow a user standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { followed_id: @other.id }
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, xhr: true, params: { 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
      delete relationship_path(relationship), xhr: true
    end
  end
end

この時点でテストはパスする。

演習

1:relationships_controller.rbrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認。

relationships_controller.rb
    respond_to do |format|
      # format.html { redirect_to @user }
      format.js
    end
ERROR["test_should_follow_a_user_standard_way", FollowingTest, 1.5789846269981354]
 test_should_follow_a_user_standard_way#FollowingTest (1.58s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:7:in `create'
            test/integration/following_test.rb:31:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:30:in `block in <class:FollowingTest>'

ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 1.6338956370018423]
 test_should_unfollow_a_user_the_standard_way#FollowingTest (1.63s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:16:in `destroy'
            test/integration/following_test.rb:45:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:44:in `block in <class:FollowingTest>'

  72/72: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.13223s
72 tests, 334 assertions, 0 failures, 2 errors, 0 skips

2:xhr: trueがある行のうち、片方のみを削除するとどういった結果になるか?
このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのかを感がてみる。

following_test.rb
  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      # post relationships_path, xhr: true, params: { followed_id: @other.id }
    end
  end
 FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.2568579829967348]
 test_should_follow_a_user_with_Ajax#FollowingTest (1.26s)
        "@user.following.count" didn't change by 1.
        Expected: 3
          Actual: 2
        test/integration/following_test.rb:36:in `block in <class:FollowingTest>'

Ajaxを用いたフォローで、postリクエストを送信していない為、フォロー数が変化せずテストが失敗する。

14.3 ステータスフィード

ステータスフィードの実装では、現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、
現在のユーザー自身のマイクロポストと合わせて表示する。

このセクションを通して、複雑さを増したフィードの実装に進んでいく。

これを実現するためには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要。

ステータスフィードの最終形のモックアップがこれ

image.png

出典:図 14.21: ステータスフィード付きのHomeページのモックアップ

14.3.1 動機と計画

ステータスフィードの基本的なアイデアはシンプル。

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

image.png

出典:図 14.22: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード

どのようにフィードを実装するのかはまだ明確ではないが、テストについてはやや明確そうなので、
まずはテストから書いていく。

このテストで重要なことは、以下の3つの条件を満たすこと。

  • フォローしているユーザーのマイクロポストがフィードに含まれている
  • 自分自身のマイクロポストもフィードに含まれている
  • フォローしていないユーザーのマイクロポストがフィードに含まれていない

まずは、MichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみる。

この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになる。

先ほどの3つの条件をアサーションに変換して、Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いていく。

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

feedメソッドはまだ定義していないのでテストは失敗する。

$ rails t

演習

1:マイクロポストのidが正しく並んでいると仮定(昇順ソート)して、データセットでuser.feed.map($:id)を実行すると、どのような結果が表示されるか?考える。

user.feed.map($:id)
=>[1,2,7,8,10]

このように、引数として受け取った自分のidと、フォローしているidが組み合わさって表示される。

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

ステータスフィードに対する要件定義は、先ほどのテストで明確になったので、
早速フィードの実装に着手する。

最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していく。

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

このクエリを模式的に書くと

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

上記のコードを書く際に、SQLがINというキーワードをサポートしていることを前提にしている。
(Railsではサポートされている)

このキーワードを使うことで、idの集合を内包(setinclusion)に対してテストを行える。

13章のプロトフィードでは、上のような選択を行うために
Active Recordのwhereメソッドを使っていることを思い出す。

この時に選択すべき対象はシンプルで、
現在のユーザーに対応するユーザーidを持つマイクロポストを選択すればよかった。

Micropost.where("user_id = ?", id)

今回必要になる選択は、上よりも少し複雑で、例えば次のような形になる。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

これらの条件から、フォローされているユーザーに対応するidの配列が必要であることがわかってきた。

これを行う方法の1つは、Rubyのmapメソッドを使うこと。

このメソッドはすべての「列挙可能」なオブジェクト
(配列やハッシュなど、要素の集合で構成されたあらゆるオブジェクト)
で使える。

なお、このメソッドは四章でも出てきた。
他の例題として、mapメソッドを使って配列を文字列に変換すると、以下のようになる。

$ rails console
>> [1,2,3,4].map { |i| i.to_s }
=> ["1","2","3","4"]

上記に示したような状況では、各要素に対して同じメソッドが実行される。

これは非常によく使われる方法であり、次のようにアンバサンド(&)と、メソッドに対応するシンボルを使った短縮表記が使える。
この短縮表記であれば、変数iを使わずに済む。

>> [1,2,3,4].map(&:to_s)
=> ["1","2","3","4"]

この結果に対してjoinメソッドを使うと、idの集合をカンマ区切りの文字列として繋げることができる。

>> [1,2,3,4].map(&:to_s).join(', ')
=> "1,2,3,4"

上記のコードを使えば、user.followingにある各要素のidを呼び出し、フォローしているユーザーのidを配列として扱うことができる。

例えばDBの最初のユーザーに対して実行すると、次のような結果になる。

>> User.first.following.map(&:id)
  User Load (0.9ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  User Load (1.0ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> [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, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

実際、この手法は実に便利なので、Active Recordでは次のようなメソッドも用意されている。

>> User.first.following.ids
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.5ms)  SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> [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, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
>> 

このfollowing_idsメソッドは、has_many :followingの関連付けをした時に、
Active Recordが自動生成したもの。

これにより、user.followingコレクション対応するidを得るためには、
関連付けの名前の末尾に_idsを付け足すだけで済む。

結果として、フォローしているユーザーidの文字列は次のようにして取得することができる。

>> User.first.following_ids.join(', ')
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ?  [["follower_id", 1]]
=> "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, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

ただ、実際のSQL文字列に挿入するときは、このように記述する必要はない。

実は、?を挿入すると自動的にこのあたりの面倒を見てくれる。

さらに、DBに依存する一部の非互換性まで解消してくれる。
つまり、ここではfollowing_idsメソッドをそのまま使えば良いだけ。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

というコードが無事に動いた。

作成したコードはこれ

user.rb
  # パスワード再設定の期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

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

  # ユーザーをフォローする
  def follow(other_user)
    following << other_user
  end

演習

1:Userモデルにおいて、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いか?
また、そのような変更を加えると、user_test.rbのど部分のテストが失敗するか?

$ user = User.first
$ user.feed
 Micropost Load (0.9ms)  SELECT  "microposts".* FROM "microposts" WHERE (user_id IN (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,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51) OR user_id = 1) ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 11]]

OR user_id = 1にて、自分自身のユーザーidを渡している点に注目。

この渡す為の処理をメソッドから削除する。

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

テストの失敗箇所

app/models/user.rb:90:in `feed'
test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>'
test/models/user_test.rb:96:in `block in <class:UserTest>'

2:Userモデルにて、フォローしているユーザーの投稿を含めないように内容にするには?
また、テストの失敗箇所を見てみる。

user.rb
  # ユーザーのステータスフィードを渡す
  def feed
    Micropost.where("user_id = ?", following_ids, id)
  end
            app/models/user.rb:90:in `feed'
            test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>'
            test/models/user_test.rb:96:in `block in <class:UserTest>'

3:フォローしていないユーザーの投稿を含めるためにはどうすればいいか?
また、そのような変更を加えると、テストがどう失敗するか?

user.rb
  # ユーザーのステータスフィードを渡す
  def feed

    Micropost.all
  end

全部含めてみる。

        Expected true to be nil or false
        test/models/user_test.rb:105:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:104:in `block in <class:UserTest>'

falseだけど〜って怒られてる。

14.3.3 サブセレクト

先ほどのフィードの実装は、投稿されたマイクロポストの数が膨大になった時にうまくスケールしない。

つまり、フォローしているユーザーが5000人程度になると、
Webサービス全体が遅くなる可能性がある。

この節では、フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していく。

following_idsでフォローしている全てのユーザーをDBに問い合わせし、
さらに、フォローしているユーザーの完全な配列を作るために再度DBに問い合わせしているのは問題である。。

feedメソッドでは、集合に内包されているかどうかだけしかチェックされていない為、この部分はもっと効率的なコードに置き換えられるはず。

また、SQLは本来このような集合の操作に最適化されている。
実際、このような問題は、SQLのサブセレクト(subselect)を使うと解決できる。

まずは、Userモデルのコードを若干修正し、フィードをリファクタリングすることから始める。

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

上記の実装では、これまでのコード

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

次のように置き換えた。

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

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

上記の説明が示すように、これからSQLクエリにもう1つのuser_idを追加する。
特に、次のコードは

following_ids

このようなSQLに置き換えることができる。

following_ids = "SELECT followed id FROM relationships
                WHERE follower_id = :user_id"

このコードをSQLのサブセレクトとして使う。

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE  follower_id = 1)
      OR user_id = 1

つまり、このような階層構造になっている。

  • user_id
    • 「ユーザー1がフォローしているユーザー全てを選択する」(サブセレクト)

このように、SELECT文を入れ子の中に内包させる形を「サブセレクト」と言う。

このサブセレクトは、集合のロジックをDBに保存するので、より効率的にデータを取得できる。

上記を元に、効率的なフィードを実装する。

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

このコードは、Rails+Ruby+SQLのコードが複雑に絡み合っているが、きちんと動作する。

14 tests, 58 assertions, 0 failures, 0 errors, 0 skips

大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期生成するなどのさらなる改善が必要だが、Railsチュートリアルではここまでの改善にしておく。(Railsの入門書なので)

これで、ステータスフィードの実装が完了した。

スクリーンショット 2019-02-09 3.14.21.png

いつも通り、masterブランチに変更を取り込む。

$ rails t
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

あとはコードをリポジトリにpushして、本番環境にデプロイ

$ git push
$ git push heroku
$ source <(curl -sL https://cdn.learnenough.com/heroku_install)
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

スクリーンショット 2019-02-09 4.11.13.png

完成したサンプルAppはこちら

新規作成アカウントの有効化ができない場合は、以下のid、passでログインしてみてください。

email:example-2@railstutorial.org
pass:password

演習

1:Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみる。

following_test.rb
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
    end
  end

2:上記コードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしている。
その理由は?
また、試しにエスケープ処理を外して、得られるHTMLの内容を調べてみて、マイクロポストの内容がおかしい点を挙げよ。

following_test.rb
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end

A:contentをエスケープしている為、CGI.esapeHTMLを加える必要がある。

14.4 最後に(サンプルアプリケーションで学んだこと)

サンプルappで学んだことをまとめてみる。

  • MVCモデル
  • テンプレート
  • パーシャル
  • beforeフィルター
  • バリデーション
  • コールバック
  • データモデルの関連付け(has_many/belongs_to/has_many through)
  • セキュリティ
  • テスティング
  • デプロイ

今後はサンプルAppに

  • 返信機能
  • メッセージ機能
  • フォロワーの通知
  • RSSフィード
  • RESTAPI
  • 検索機能
  • いいね機能
  • シェア機能

などを加えてオリジナルアプリケーションを完成させていくと良い。

終わったー

やっとRailsチュートリアルを終えることができました。
長かったですねー。

全部Qiitaにメモったおかげで、流し読みせずに熟読できました。
お陰で、全体を通してWebアプリケーション制作の基礎を理解できたような気がします。

正直、1周目の理解度は30パーセントぐらいでした。
今回の2周目で理解度は80パーセントぐらいまで上がった気がします。

これからはオリジナルのWebアプリケーションを作成していくことで、
今回覚えた内容を自分の物にしていきたいと思います!

みなさんお疲れ様でした!

YUUKI.

単語集

  • has_many through

多対多の関係性を定義する関連付けメソッド。

  • source

has_manyに対してパラメータを与えるオプション。
sourceオブションで与えた値は配列の元を表しているので、実際の配列のインデックスは変わらない。

  • collection

コレクションルーティングを追加するメソッド。
idを指定せずに全てのメンバーを表示したりできる

スクリーンショット 2019-02-07 17.05.39.png

  • Ajax(エイジャックス)

Asynchronous(非同期な) JS + XMLで作られている非同期通信を行いながらインターフェイスの構築を行うプログラミング手法のこと。

サーバーからのレスポンスを待たずにクライアント側の表示を変更させることができる。例えば、Ajaxを使用することで画面遷移せずにHTMLを更新することが可能で、ユーザビリティの向上やサーバー負荷の軽減に繋がる。

Railsでは、remote: trueで使える。

  • respond_to

リクエストの種類によって応答を場合分けするメソッド。
処理内に書いたいずれかの1行が実行されるよう書くことができる。

書き方の例

respond_to do |format|
  format.htlm {...}
  format.json {...}
  • xhr

Xmlhttprequestの略で、Ajax通信かどうかを判定するオプション。
trueを渡すことでAjaxでリクエストを発行出来る。

Ajax

  • DOM

Document Object Modelの略で、JSファイル内で別のHTMLファイルなどを読み込む時の役割のこと。

  • following_ids

followしているユーザーのidをそれぞれ文字列に変換して、,で区切る値として返すメソッド。

  • サブセレクト

SQLのSELECT文を入れ子にしたものを指す。
入れ子構造にすることで、より効率的にデータを取得できる。

13
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9