【自分用】rails チュートリアル 14章



やること
ユーザーをフォロー、フォロー解除する仕組み追加
フォローしているユーザーの投稿をステータスフィードに表示させる
・ユーザー間の関係性をどうモデリングするか
・モデリング結果に対応するWebインターフェースを実装
・ステータスフィードの完成版実装
流れ
ユーザーAが自分のプロフィールページを最初に表示
フォローするユーザーを選択するためUsersページに移動
AはBを表示し、[Follow]ボタンを押してフォローする
[Follow]ボタンが[Unfollow]に変わり、Bの[followers]カウントが1増える
AがHomeページに戻ると[following]カウントが１増え、Bのマイクロポストがステータスフィードに表示されるようになる

14.1 Relationshipモデル
データモデルを構成する
has_many関連付けを用いると壁に当たる
has_many throughを使う
$ git checkout -b following-users

14.1.1 データモデルの問題(および解決策)
あるユーザーをフォローしている全てのユーザーの集合 followers
あるユーザーがフォローしている全てのユーザーの集合 following
user.followingはユーザーの集合でなければならない
followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければならない
さらに、それぞれの行はユーザーなので、これらのユーザーに名前やパスワードなどの属性も追加する必要がある
図14.6
上記データモデルには何かと無駄が多い
（followingモデルにも名前やメールアドレスがありusersテーブルと被っている）
さらにfollowersのモデリングをするときも同じくらい無駄の多いテーブルを別に作らなくてはならなくなる
メンテナンスの観点から見てそれは良くない
現状ユーザー名を変更するだけでusersテーブルのほかにfollowing,follwers両テーブルのそのユーザーが含まれる全ての行を更新しなければならない

必要な抽象化を行う
Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考える
1.あるユーザーが別のユーザーをフォローするとき何が作成されるか
2.あるユーザーが別のユーザーをフォロー解除するとき何が削除されるか
→関係（リレーションシップ）が作成されたり削除されたりする
一人のユーザーは1対多の関係を持てる
ユーザーはリレーションシップを経由して多くのfollowing（またはfollowers）と関係を持つことができる

左右非対称な関係性
AはBをフォローしていても、BはAをフォローしていないといった関係が成り立つ
それぞれを能動的関係 (Active Relationship)と受動的関係 (Passive Relationship)と呼ぶ
上記の例で言えばAはBに対して「能動的関係」を持っている。逆に、BはAに対して「受動的関係」を持っている

フォローしているユーザーを生成するために、能動的関係に焦点を当てていく
フォローしているユーザーはfollowed_idがあれば識別することができる
followed_idを通して、usersテーブルのフォローされているユーザーを見つけるようにする
図14.7

Relationshipデータモデルを実装
$ rails generate model Relationship follower_id:integer followed_id:integer

リスト14.1
このリレーションシップは今後follower_idとfollowed_idで頻繁に検索することになるので、それぞれのカラムにインデックスを追加
$ rails db:migrate

複合キーインデックスとは
上記の例で言えばfollower_idとfollowed_idの組み合わせが必ず一意であることを保証する仕組み
同じユーザーを2回以上フォローすることを防ぐ

演習
1. id=1のユーザーのfollowed_idを配列で返す[2,7,10,8]
2. id=2のユーザーのfollowed_idと関連付けられているidのユーザーを返す[User id:1, name:Michael Hartl, email:mhartl@example.com]
id=2のユーザーのfollowed_idを配列で返す[1]


14.1.2 User/Relationshipの関連付け
UserとRelationshipの関連付け
1人のユーザーにはhas_many（１対多）のリレーションシップがある
これは2人のユーザー間の関係なのでフォローしているユーザーとフォロワーの両方に属する(belongs_to)
次のようなコードで新しいリレーションシップを作成
user.active_relationships.build(followed_id: ...)

ユーザーとマイクロポストの関連付けとの違い
↑の時のUserモデルのコード
class User < ApplicationRecord
  has_many :microposts
Railsは引数の;micropostsシンボルに対応するMicropostモデルを探すことができた
が、今回同じように書くと
has_many :active_relationships
RelationshipモデルではなくActiveRelationshipモデルを探してしまう
クラス名を明示的に伝える必要がある

↑の時のMicropostモデルのコード
class Micropost < ApplicationRecord
  belongs_to :user
Railsはデフォルトでは外部キーの名前を<class>_idといったパターンとして理解し、 <class>に当たる部分からクラス名 (正確には小文字に変換されたクラス名) を推測する
micropostテーブルのuser_idをたどってユーザーを特定できる
が、今回はfollower_idという外部キーを使ってフォロワーを特定したいがfollowerというクラスはない
クラス名を明示的に伝える必要がある

リスト14.2 １対多(has_many)の関連付けを実装Userモデル
ユーザーを削除したらユーザーのリレーションシップも同時に削除されるよう、関連付けにdependent: :destroyも追加
Userモデルとactive_relationshipsは1対多の関係性
クラスはRelationship、外部キーはfollower_id

14.3 belongs_toの関連付けRelationshipモデル
Relationshipモデルとfollowerは１対１の関係
Relationshipモデルとfollowedは１対１の関係
クラスはUser

演習1.
user = User.first
other_user = User.second
userがother_userをフォローしている状態を作成/登録
active_relationship = user.active_relationships.create(followed_id: other_user.id)


14.1.3 Relationshipのバリデーション
検証を追加する
リスト14.4 Relationモデルのバリデーションテスト
michaelがarcherをフォローしているrelationshipオブジェクトを@relationshipに代入
test "should be valid"
　@relationshipが有効であるか
test "should require a follower_id"
　@relationshipのfollower_idにnilを代入
　@relationshipは無効である
test "should require a followed_id"
　@relationshipのfollowed_idにnilを代入
　@relationshipは無効である

リスト14.5 Relationshipモデルに対するバリデーション
follower_idとfollowed_idが存在するか


14.1.4 フォローしているユーザー
followingとfollowersに取り掛かる
has_many throughを使う
ユーザーには複数の「フォローする」「フォローされる」という多対多の関係性がある
has_many throughの関連付けではモデル名(単数形)に対応する外部キーを探す
has_many :followeds, through: :active_relationship
上記のコードは「followeds」というシンボル名を「followed」という単数形に変えて、relationshipテーブルのfollowed_idを使って対象のユーザーを取得する働きを持つ
が、「user.followeds」は英語として不適切なので「user.following」に改名して使いたい
→そのためにRailsのデフォルトを上書きする
:sourceパラメーターを使って「following配列の元はfollowed idの集合」であると明示的にRailsに伝える

リスト14.8 Userモデルにfollowingの関連付けを追加
Userとfollowingはactive_relationshipを通じて多対多の関係性
関連付けfollowing元はfollowed

フォローしているユーザーを配列のように扱えるように
例えば
user.following.include?(other_user)
user.followingにother_userが含まれているか（user.followedから探す）

user.following.find(other_user)
user.followingからother_userを取得（user.followedから探す）

followingで取得したオブジェクトは配列のように要素を追加したり削除したりできる
user.following << other_user
user.followingの最後にother_userを追加

user.following.delete(other_user)
user.followingからother_userを削除

following.include?(other_user)
このコードではDBから全てのユーザーを取得してメソッドを実行しているのではなく、DBの中で直接比較している。

followingで取得した集合を簡単に取り扱うために、followメソッドとunfollowメソッドを追加する
あるユーザーが誰かをフォローしているかどうかを確認するfollowing?論理値メソッドも追加する
これらのメソッドを追加してもすぐに使える場面がく手応えを感じにくい
よって簡単かつすぐできるテストから書いていく

リスト14.9 following関連のメソッドをテスト
流れ
1.following?メソッドで、あるユーザーをまだフォローしていないことを確認
2.followメソッドを使ってそのユーザーをフォロー
3.following?メソッドでフォロー中になったか確認
4.unfollowメソッドでフォロー解除できたか確認

リスト14.10 following関連のメソッド
なるべくselfを省略する(model内では省略できるため)
def follow
followingの最後にother_user追加

def unfollow
active_relationshipsからfollowed_idがother_user.idのデータを取得し削除

def following?
followingにother_userが含まれているか


14.1.5 フォロワー
user.followingメソッドと対になるuser.followersメソッドを追加
リスト14.12
Userモデルと:passive_relationshipsはhas_many(1対多)の関係性
クラスはRelationship、外部キーはfollowed_id、ユーザーが削除された時紐づいているpassive_relationshipsも削除される
Userfollowingはpassive_relationshipsを通じて多対多の関係性
関連付け(followers)元の名前はfollower source: :followerは省略可能
←followersはfollowedと違いfollowersを単数形にして自動でfollower_idを探索できるため
しかし類似性を強調するため省略せず書いている

リスト14.13 followers.include?メソッドを使いデータモデルをテスト
archerのフォロワーにmichaelが含まれている

演習
3 to_aメソッドを使うと配列を生成してからカウントしているので時間がかかる上、DBに負担がかかる


14.2 [Follow]のWebインターフェース
フォロー/フォロー解除の基本的なインターフェースを実装
フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する

14.2.1 フォローのサンプルデータ
rails db:seedを使ってDBにサンプルデータを自動生成する
先にサンプルデータを自動作成しておけばバックエンド機能の実装を後回しにしてWebページの見た目のデザインから取り掛かれる

リスト14.14 リレーションシップのサンプルデータ
usersに全てのユーザー代入
userにUserテーブルの最初のユーザー追加
followingにusersの3~51番目を代入
followersにusersの4~41番目を代入
followingを順に取り出しfollowedに代入、userがfollowedをフォローする
followersを順に取り出しfollowerに代入、followerがuserをフォローする

DB上のサンプルデータに反映させる
$ rails db:migrate:reset
$ rails db:seed


14.2.2 統計と[Follow]フォーム
Homeページとプロフィールページにフォローしているユーザーとフォロワーを反映させる
1. フォローしているユーザーとフォロワーの統計情報を表示するパーシャルを作る
2. フォロー用とフォロー解除用のフォーム作成
3. フォローしているユーザーとフォロワーのそれぞれの一覧ページ作成

リスト14.15 ルーティング実装
統計情報をクリックすると一覧ページに飛ぶ
実際のページは後ほど
resources :usersにネスト追加
memberメソッドでユーザーidが含まれるURL使用可能に
:following、:followersのgetのルーティング生成

リスト14.16 統計情報を表示するパーシャル
@userに@user、@userがnilの場合にはcurrent_userを代入
Homeページとプロフィールページに表示させるため現在のユーザーを取得
フォロー数、フォロワー数のカウントはDB内で行う

リスト14.17 Homeページにフォロワーの統計情報を表示させる
リスト14.18 scss追加

リスト14.19 follow/unfollowフォームのパーシャル
@userがログインユーザー(current_user)でなければ表示させる
@userをログインユーザー(current_user)がフォローしていたらunfollow
そうでなければfollow
followとunfollowのパーシャルに振り分けているだけ

リスト14.20 Relationshipsリソース用のルーティング生成
relationshipsのcreateとdestroyのルーティング生成

リスト14.21 フォロー用フォーム
current_userと結びつけた新しいRelationshipオブジェクトを返す
followed_idに@user.idを渡す

リスト14.22 フォロー解除フォーム
current_userと結びついたリレーションシップからfollowed_idが@user.idのデータ取得
deleteメソッド指定
どちらもform_forでRelationshipモデルオブジェクトを操作する
これらのフォームにはボタンしかないが、followed_idをコントローラに送信する必要がある
そのためにhidden_field_tagメソッドを使う
<input id="followed_id" name="followed_id" type="hidden" value="3" />
パスワードの再設定フォームの時のように
隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることが出来る

リスト14.23 プロフィールページにフォロー数/フォロワー数の統計表示とフォロー/フォロー解除用フォームを表示させる

演習
3 Homeページとプロフィールページのテスト追加
strong id="following"タグがあるか
描写されたページに@user.following.countを文字列にしたものがあるか
strong id="followers"タグがあるか
描写されたページに@user.followers.countを文字列にしたものがあるか


14.2.3 [Following]と[Followers]ページ
フォローしているユーザーとフォロワーを表示するページを作る
まずはフォローしているユーザーのリンクとフォロワーのリンクを動くようにする
どちらもユーザーのログインを要求する

リスト14.24 フォロー/フォロワーページの認可テスト
/users/@user.id/followingにgetリクエスト
login_urlにリダイレクト
/users/@user.id/followersにgetリクエスト
login_urlにリダイレクト

このままではREDなのでUsersコントローラに２つ新しいアクションを追加する
それぞれのアクションでタイトルを設定、ユーザーを検索、@user.followingまたは@user.followerからデータを取り出し、ページネーションをしてページを出力する必要がある

リスト14.25 followingアクションとfollowersアクション
指定したアクションの実行前に、AplicationControllerのlogged_in_userメソッドを実行
←ここにfollowingとfollowersアクションを追加
def following
@titleにfollowing_titleを代入
@userにDBから取得したparams[:id]のuser代入
@usersに@user.followingのページネーション代入
show_followビューを出力
def followers
↑とほぼ同じ
@usersに@user.followersのページネーション代入のみ異なる

Railsは慣習に従ってアクションに対応するビューを暗黙的に呼び出す(showアクションの最後にshow.html.erbを呼び出すなど)
しかし↑ではshow_followを明示的に呼び出している

リスト14.26 show_followビュー
実行されるアクションによってフォローしているユーザーもしくはフォロワーの一覧が表示される
現在のユーザーを使っていないため、他のユーザーのフォロワー一覧ページも動く

show_followビューに対する統合テスト作成
$ rails generate integration_test following
HTML構造を網羅的にチェックするテストは壊れやすい
したがって、正しい数が表示されているか、正しいURLが表示されているかの２つをテストする
リスト14.28 テスト用にリレーションシップfixtureにデータ追加

リスト14.29 following/followersページのテスト
def setup
@userにmichael代入
ログインする

test following page
/users/@user.id/followingにgetリクエスト
@userのfollowingが空でない
@user.followingの数を文字列に変換したものが本文に一致する
@user.followingを順に取り出しuserに代入
    a href="/users/userのid" のaタグが存在する

test followers page
↑ほぼ同じ followingをfollowersに変える


14.2.4 [Follow]ボタン(基本編)
Follow/Unfollowボタンを機能させる
フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているので、Relationshipコントローラが必要
$ rails generate controller Relationships

リレーションシップのアクセス制御とそれらのテスト
Relationshipsコントローラのアクションにアクセスするときログイン済みかテスト
リスト14.31
create should require logged-in user
下記動作前後でリレーションシップの数に違いがない
  relationships_pathにpostリクエスト
login_urlにリダイレクト

destroy should require logged-in user
下記動作前後でリレーションシップの数に違いがない
  oneのidのrelationship_pathにdeleteリクエスト
login_urlにリダイレクト

テストをパスするためlogged_in_userフィルターをRelationshipsコントローラに追加
リスト14.32

フォームから送信されたパラメータを使い、followed_idに対応するユーザーを見つける
その後、そのユーザーにフォローまたはunfollowメソッドを使う
リスト14.33
create
params[:followed_id]のデータをUserテーブルから取得し、userに代入
current_userでuserをフォロー
userのページにリダイレクト

destroy
Relationshipテーブルからfollowedカラムがparams[:id]のデータを取得し、userに代入
current_userでuserをアンフォロー
userのページにリダイレクト

logged_in_userフィルターはなくても支障はないが、セキュリティのために設定しておく


14.2.5 [Follow]ボタン（Ajax編）
Ajaxを使いWebページからサーバーに非同期でページを移動せずにリクエストを送信する
リスト14.34 14.35
localをfalseに変更
これでrailsはAjaxを使うように

Relationshipsコントローラを、Ajaxリクエストに対応できるよう改良
リクエストの種類によって応答を場合分けするときは、respond_toメソッドを使う
リスト14.36
リクエストの種類によってブロック内の一文が実行される
Ajaxを使うことによりビューで必要になるため、ローカル変数userをインスタンス変数@userに変更

ブラウザでJavaScriptが無効になっていた場合でも動くように
リスト14.37
認証トークンをremoteフォームに埋め込む

フォロー/フォロー解除したときにプロフィールページを更新するためJavaScript用の埋め込みRuby(.js.erb)ファイルを編集
リスト14.38
_follow_form.html.erb内のid="follow_form"の中にrender('users/unfollow')HTML挿入
_stats.html.erb内のid="followers"の中に<%= @user.followers.count %>HTML挿入

リスト14.39
_follow_form.html.erb内のid="follow_form"にrender('users/follow')HTML挿入
_stats.html.erb内のid="followers"の中に<%= @user.followers.count %>HTML挿入

escape_javascriptメソッドでJavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープする
文字化けによる誤作動を避けることができる
プロフィールページを更新せずにフォロー/フォロー解除ができるようになった


14.2.6 フォローをテストする
フォローに対するテストでは/relationshipsに対してPOSTリクエストを送り、フォローされたユーザーが1人増えたことを確認
通常のコントローラからアクションを呼び出す形式のフォロー/フォロー解除のテストと、Ajaxを利用したテストも必要
→xhr:trueオプションを追加する
フォロー解除の時は↑のPOSTをdeleteに置き換えて、フォローしている数が1人減っているのを確認

リスト14.40
setup内で@userにarcher代入
should follow a user the standard way
下記の動作後@userのフォロー中数が１増えている
  relationships_pathにpostリクエスト@otherをフォローする

should follow a user with Ajax
下記の動作後@userのフォロー中数が１増えている
  relationships_pathにAjaxでpostのリクエスト（@other をフォロー

should unfollow a user the standard way
@userで@otherをフォロー
relationshipにDBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータを代入
下記の動作後@userのフォロー中数が１減っている
  relationship_pathにdeleteのリクエスト（relationshipを削除する）

should unfollow a user with Ajax
@userで@otherをフォロー
relationshipにDBの@userのactive_relationshipsからfollowed_id:が@other.idと一致するデータを代入
下記の動作後@userのフォロー中数が１減っている
  relationship_pathにAjaxでにdeleteのリクエスト（relationshipを削除する）


14.3 ステータスフィード
フォローしているユーザーと自分自身の投稿を表示する

14.3.1 動機と計画
現在のユーザー現在のユーザーによってフォローされているユーザーに対応するユーザーIDを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出す

フィードのテスト
1.フォローしているユーザーのマイクロポストがフィードに含まれている
2.自分自身のマイクロポストもフィードに含まれている
3.フォローしていないユーザーのマイクロポストがフィードに含まれていない
リスト14.42
michael:lanaをフォローarcherはフォローしていない
fixtures/relationships.yml

lanaのmicropostsを順に取り出してpost_followingに代入
  michaelのfeedにpost_followingが含まれている
michaelのmicropostsを順に取り出してpost_selfに代入
  michaelのfeedにpost_selfが含まれている
archerのmicropostsを順に取り出してpost_unfollowedに代入
  michaelのfeedにpost_unfollowedが含まれていない

テストはまだRED
演習
投稿日時が古い順に並んでしまう

14.3.2 フィードを初めて実装する
必要なクエリは、micropsotsテーブルからあるユーザーがフォローしているユーザーに対応するidを持つマイクロポストを全て選択すること
これをコードにすると↓
micropostsテーブルを検索
SELECT * FROM microposts
user_idに<list of ids>かuser_id = <user id>を含むものを取得
WHERE user_id IN (<list of ids>) OR user_id = <user id>

13.3.3のプロトタイプでは現在のユーザーに対するマイクロポストを選択した
Micropost.where("user_id = ?", id)
が、今回は少し複雑になる
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
これらの条件からフォローされているユーザーに対するidの配列が必要
→mapメソッドで実装
このメソッドは全ての「列挙可能(enumerable)」なオブジェクト(配列やハッシュなど、要素の集合で構成されるあらゆるオブジェクト)で使える

配列を文字列に変換して一つずつ取り出す
>>[1,2,3,4].map { |i| i.to_s }
=>["1", "2", "3", "4"]

短縮表記
>>[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を配列として扱える

>> User.first.following.map(&:id)
=> [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
=> [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(',')
=> "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"

実際はRailsが上手いことしてくれるので
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

リスト14.44
Micropostテーブルから　user_idにフォローしているユーザーのidか現在のユーザーのidを持つもの　を取得

演習
1. Micropost.where("user_id IN (?)", following_ids)
2.Micropost.where("user_id = ?", id)
3.Micropost.all


14.3.3 サブセレクト
現在のフィードではマイクロポストの数が膨大になったときにWebサービスが重くなる
現状の問題点:following_idsでフォローしている全てのユーザーをDBに問い合わせ、さらにフォローしているユーザーの完全な配列を作るために再度DBに問い合わせている点

SQLのサブセレクトを使うことで解決する
サブセレクトとは、入れ子になったセレクト文のこと

following_idsをSQLに置き換えると
following_ids = "SELECT followed_id FROM relationships
                 WHERE  follower_id = :user_id"
 
上のコードを使用したサブセレクトはこう
SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE  follower_id = :user_id)
      OR user_id = :user_id

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

演習
１  test "feed on Home page"
root_pathにgetのリクエスト
@userのfeedのページネート1ページ目から配列を一つずつ取り出してmicropostに代入
  response.bodyに特殊文字をエスケープしたmicropost.contentが含まれている

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

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed