#近況報告
エンジニア転職成功しました。YouTubeもはじめました。
著者略歴
著者:YUUKI
ポートフォリオサイト:Pooks
現在:RailsTutorial2周目
#第14章 ユーザーをフォローする 難易度 ★★★★ 5時間
挫折しないRailsチュートリアルの進め方を先にお読みください↓↓
この章では、他のユーザーをフォローしたり、フォロー解除したりするソーシャル的な仕組みと、
フォローしているユーザーの投稿をステータスフィード(いわゆるタイムライン)
に表示する仕組みを追加する。
そのために、まずはユーザー間の関係性をどうモデリングするかについて学ぶ。
その後、モデリング結果に対応するWebインターフェースを実装していく。
Webインターフェースの例としてAjaxについても後に詳解する。
最後に、ステータスフィードの完成版を実装する。
この最終章では、本書の中で最も難易度の高い手法をいくつか使っている。
その中には、ステータスフィードの作成のためにRuby/SQLを騙すテクニックも含まれる。
この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。
ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ。
この章で学ぶことは今まで最も難易度が高いため、
コードを書く前に一旦インターフェースの流れを理解する。
モックアップはこちら
出典:図 14.3: ユーザーのプロフィール画面に [Follow] ボタンが表示されている
出典:図 14.4: プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた
出典:図 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)などの属性を追加する。
出典:図 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テーブルのフォローされているユーザーを見つけるようにする。
このデータモデルを模式図にすると、以下のようになる。
出典:図 14.7: 能動的関係をとおしてフォローしているユーザーを取得する模式図
間にactive_relationships
を挟むことで、フォローとフォロワーの関係性がスムーズに繋がっている。
能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになる。
したがって、テーブル名にはこの「関係」を表す「relationships」を使う。
モデル名はRailsの慣習にならって、Relationshipとする。
作成したRelationshipデータモデルを以下に示す。
1つのrelationshipsテーブルを使って2つのモデル(能動的関係と受動的関係)をシミュレートする方法については後に説明する。
このデータモデルを実装するために、まずは上記のデータモデルに対応したマイグレーションを生成する。
$ rails g model Relationship follower_id:integer followed_id:integer
このリレーションシップは今後follower_id
とfollowed_id
で頻繁に検索することになるので、
それぞれのカラムインデックスを追加する。
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_id
とfollowed_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の関連付けは以下のようになる。
class User < ApplicationRecord
# 関連付け
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
明示的にclass名や外部キー、destroyも追加している。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があるため)
class Relationship < ApplicationRecord
# 1対1の関連付け
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
なお、followerの関連付けはまだ使わない。
(書いておくことで構造の理解の手助けになる)
user.rb
とrelationship.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ファイルを空にしておく。
# 空にする
早速、簡単なテストとバリデーションを記入する。
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
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をコメントアウトしてもテストが成功することを確認。
# 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の関連付けの核心following
とfollowers
に取りかかる。
今回は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に伝える。
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で取得した場合をより簡単に取り扱うために、
follow
やunfollow
といった便利メソッドを追加する。
これらのメソッドは、例えばuser.follow(other_user)
といった具合に使う。
さらに、これに関連するfollowing?
論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにする。
今回は、こういったメソッドはテストから先に書いていく。
と言うのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから。
一方で、Usermモデルに対するテストは書くのは簡単かつ今すぐできるので、
先に書いていく。
具体的には、
-
following?
メソッドであるユーザーをまだフォロしていないことを確認 -
follow
メソッドを使ってそのユーザーをフォローできたことを確認 -
unfollow
メソッドでフォロー解除できたことを確認
といった具合でテストしていく。
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
による関連付けを使ってfollow
、unfollow
、following?
メソッドを実装していく。
このとき、可能な限りself
を省略している点に注目。
# ユーザーをフォローする
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_id
とfollowed_id
を入れ替えるだけで、
フォロワーについてもフォローする場合と全く同じ活用が出来る。
データモデルは以下。
出典:図 14.9: Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル
要は、active_relationships
をpassive_relationships
に入れ替えて、
followed_id
とfollower_id
を入れ替えるだけ。
上記のデータモデルの実装をuser.rb
にhas_manyを使って行う。
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?
メソッドを定義してもよかったが、
サンプルアプリケーションで実際に使う場面がなかったのでこのメソッドは省略している。
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に最初のユーザーをフォローさせる。
$ 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
といった具合に表示する。
上記の統計情報には、現在のユーザーがフォローしている人数と、
現在のフォロワーの人数が表示されている。
それぞれの表示はリンクになっており、専用の表示ページに移動できる。
これらのリンクはダミーテキスト#
を使って無効にしていた。
しかし、ルーティングについての知識もだいぶ増えてきたので、今回は実装することにする。
実際のページ作成は後にルーティングは今実装する。
このコードでは、resources
ブロックの内側で:member
メソッドが使っている。
これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。
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つのリンクを含めるようにする。
<% @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ページにこの統計情報を表示するには、以下のようにすると良い。
<% @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ページは以下のようにする。
/* 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;
}
出典:図 14.11: Homeページにフォロー関連の統計情報を表示する
この後すぐ、プロフィールにも統計情報パーシャルを表示するが、
今のうちに[Follow]/[Unfollow]ボタン用のパーシャルを作成する。
<!--現在のユーザーが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リソースの例に従って作成する。
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
フォロー/フォロー解除用のパーシャルも書く。
<%= 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 %>
<%= 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を書く。
</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]ボタンが表示される。
これらのボタンを実装するには、二通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。
でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。
####演習
1:ブラウザから/users/2
にアクセスし、フォローボタンが表示されていることを確認する。
同様に、/users/5ではUnfollow]ボタンが表示されているはず。
さて、/users/1
にアクセスすると、どのような結果が表示されるか?
users/1はログインユーザーなのでボタンが消える。
2:ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認。
確認済み。
3:Homeページに表示されている統計情報に対してテストを書いてみる。
同様にして、プロフィールページにもテストを追加してみる。
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
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]ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。
どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。
出典:図 14.14: フォローしているユーザー用ページのモックアップ
出典:図 14.15: ユーザーのフォロワー用ページのモックアップ
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。
Twitterに倣って、どちらのページでもユーザーのログインを要求するようにする。
そこで前回のアクセス制御と同様に、まずはテストから書いていく。
今回使うテストは以下の通り。
上記コードではfollowing/followers
の名前付きルートを使っている点に注意。
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.follower
sからデータを取り出し、ページネーションを行なって、ページを出力する必要がある。
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つのファイルで両方の場合をカバーできるから。
<% 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
ビューを呼び出す。
この時、上記コードでは現在のユーザーを一切使っていないので、
他のユーザーのフォロワー一覧ページもうまく動く。
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にテストデータを追加する。
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かどうかをテストするコードも加えると、以下のようになる。
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
を開き、それぞれが適切に表示されていることを確認。
サイドバーにある画像は、リンクとしてうまく機能しているか?
OK
2:following_test
のassert_select
関連のコードをコメントアウトしてみて、正しくテストが失敗することを確認。
<% @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のカウントが変わっていないことを確認する。
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コントローラのアクションに対して追加する。
class RelationshipsController < ApplicationController
before_action :logged_in_user
def create
end
def destroy
end
end
[Follow]/[Unfollow]ボタンを動作させるためには、フォームから送信されたパラメータを使って、followed_id
に対応するユーザーを見つけてくる必要がある。
その後、見つけてきたユーザーに対して適切にfollow/unfollow
メソッド(Userモデルで定義した)を使う。
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_user
はnil
になり、
どちらのメソッドでも2行目で例外が発生する。
エラーにはなるが、アプリケーションやデータに影響は生じない。
このままでも支障はないが、このような例外には頼らない方がいいので、セキュリティの為のレイヤーを追加した。
これで、フォロー/フォロー解除の機能が完成した。
どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。
(振る舞いを検証する統合テストはのちに実装する)
フォローしていないユーザーの画面
ユーザーをフォローした結果
####演習
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を使うようになる。
<%= 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 %>
<%= 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による)
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が無効になっていた場合でもうまく動くようにする。
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を挿入する時に実行結果をエスケープするために必要。
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
各行の末尾にセミコロン;があることに注目。
(これは1950年代中頃に開発されたALGOLまで遡るらしい)
destroy.js.erb
ファイルの方も同様です。
$("#follow_form").html("<%= escape_javascript(render(`users/unfollow`)) %>");
$("#followers").html('<%= @user.followers.count %>');
$("#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
これらのテストをまとめた結果
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.rb
のrespond_to
ブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認。
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
がある行のうち、片方のみを削除するとどういった結果になるか?
このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのかを感がてみる。
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プログラミングの技術も必要。
ステータスフィードの最終形のモックアップがこれ
出典:図 14.21: ステータスフィード付きのHomeページのモックアップ
###14.3.1 動機と計画
ステータスフィードの基本的なアイデアはシンプル。
以下の図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すこと。
出典:図 14.22: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード
どのようにフィードを実装するのかはまだ明確ではないが、テストについてはやや明確そうなので、
まずはテストから書いていく。
このテストで重要なことは、以下の3つの条件を満たすこと。
- フォローしているユーザーのマイクロポストがフィードに含まれている
- 自分自身のマイクロポストもフィードに含まれている
- フォローしていないユーザーのマイクロポストがフィードに含まれていない
まずは、MichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみる。
この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになる。
先ほどの3つの条件をアサーションに変換して、Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いていく。
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)
というコードが無事に動いた。
作成したコードはこれ
# パスワード再設定の期限が切れている場合は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を渡している点に注目。
この渡す為の処理をメソッドから削除する。
# ユーザーのステータスフィードを渡す
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モデルにて、フォローしているユーザーの投稿を含めないように内容にするには?
また、テストの失敗箇所を見てみる。
# ユーザーのステータスフィードを渡す
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:フォローしていないユーザーの投稿を含めるためにはどうすればいいか?
また、そのような変更を加えると、テストがどう失敗するか?
# ユーザーのステータスフィードを渡す
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モデルのコードを若干修正し、フィードをリファクタリングすることから始める。
# ユーザーのステータスフィードを渡す
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に保存するので、より効率的にデータを取得できる。
上記を元に、効率的なフィードを実装する。
# ユーザーのステータスフィードを渡す
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の入門書なので)
これで、ステータスフィードの実装が完了した。
いつも通り、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
新規作成アカウントの有効化ができない場合は、以下のid、passでログインしてみてください。
email:example-2@railstutorial.org
pass:password
####演習
1:Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみる。
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の内容を調べてみて、マイクロポストの内容がおかしい点を挙げよ。
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を指定せずに全てのメンバーを表示したりできる
- 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文を入れ子にしたものを指す。
入れ子構造にすることで、より効率的にデータを取得できる。