プログラミング年少組のど素人が、アウトプット用に素人目線の解説を書いていきます。
自分が自分に教えていることを想像しながら進めていきます。
下記と同じような境遇の方であれば参考になるかも
- まっさらなプログラミング初学者目線
- オール独学
- 勉強は得意じゃない
- 社会人のため、1日に勉強できる量は限られる。
電子書籍のwebテキスト(第6版)を購入して使用
Progateの「Web開発パス」完走済
ドットインストールのプレミアム会員卒業
Railsチュートリアル解説動画を見ながら学習中
#第14章 ユーザーをフォローする#
ついにここまで来ました・・・ラストの章です。
最後の章で実装するのは以下の2点。
①他のユーザーをフォローする機能
②フォローしたユーザーの投稿を自分のページに表示する機能
また、この最終章では、テキストの中でももっとも難易度の高い手法をいくつか使っているそうです。7章あたりから、ここからは難易度が上がると言い続けてきたような・・・難易度のインフレが止まりません。
まあ、ラストダンジョンでの敵が弱いわけがありませんのでここも気合で乗り切りましょう!
#14.1 Relationshipモデル#
とにもかくにも、新しい機能の搭載なのでまずはトピックブランチを作成します
$ git checkout -b following-users
##14.1.1 データモデルの問題(および解決策)##
フォローについて考えると2つの状態が考えられます
・誰をフォローしているか(follower)
・誰にフォローされているか(followed)
この2つの視点から機能を実装していく必要がありそうです。
この2つでテーブルを作成した場合の問題:無駄が多くなりがち。
「誰をフォローしているかテーブル」・「誰にフォローされているかテーブル」の双方を作成していると、データの修正が生じた際に、両方で処理を走らせなくてはならなくなります。
なぜならそれぞれのテーブルで同じカラムが存在するためです。
具体例で考えると、「usersテーブル」「誰をフォローしているかテーブル」「誰にフォローされているかテーブル」全てにおいてemailカラムが存在します。
ユーザーがemailを変更しようとした際にこの3つのテーブルにおいて修正が生じる。ということになります。
これを解決するにはまず以下の2つの観点に注目します
1.あるユーザーが別のユーザーをフォローする時、何が生成されるか
2.あるユーザーが別のユーザーのフォローを解除する時、何が削除されるか
要は抽象化して、2者の**「関係性」**に着目しましょうということみたいです。
また、フォローの関係性にも色々あります。
Facebookなどは友達登録を行うと確実に相互にフォローし合う性質
Twitterのように片方だけフォローしているケースも考えられる性質
今回は後者の組み立てを行っています。
フォローする側を能動的関係(Active Relationship)、フォローされる側を**受動的関係(Passive Relationship)**と言います。
ごちゃごちゃ書きすぎると余計分からなくなりますね
結論:重複したカラムを取り除いた「Relationshipテーブル」を作ります。
そしてそれを、能動的関係の視点や受動的関係の視点と関連付けて使うということです。
$ rails generate model Relationship follower_id:integer followed_id:integer
ここで作成した2つのカラム「follower_id」「followed_id」は頻繁に検索しますので今作成されたマイグレーションファイルにインデックスを加えます
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
最後のこちらについてです
add_index :relationships, [:follower_id, :followed_id], unique: true
補足で書いてますが、こうすることで、「あるユーザーが同じユーザーを2回以上フォローすることを防ぎます」
以前、emailの重複を阻止するために、このような形でindexを用いました。
こうすることで、ブラウザを介さないような裏技でフォローを増やすような行為も防止することができます。
これでテーブルの元ができましたので、実際に作成に移りましょう
$ rails db:migrate
##14.1.2 User/Relationshipの関連付け##
それではフォローする側である**能動的関係(Active Relationship)**視点から実装していきましょう。
1人のユーザーは複数ユーザーをフォローすることができるのでUserモデルに
has_many :active_relationships
と書きたくなります。が、これだけでは通りません。なぜでしょう・・・
ちょうどUserモデルに同じようなコードが書いてありますのでこれを見ながら理由を考えましょう
has_many :microposts
これはユーザーが複数の投稿を持つことを表していますよね
その投稿はどこから見つけるかと言うと「Micropost」モデルです。というより「Micropost」クラスです。
Railsはデフォルトでは「has_many :microposts」と書かれていたら、「Micropost」クラスからデータを参照するように設定されています。
と、いうことは「has_many :active_relationships」なんて書かれていたら、「Active_Relationship」クラスからデータを参照しようとします。「Relationship」モデルの「Relationship」クラスなら先ほど作成しましたが、「Active_Relationship」クラスなんてものはありません。そういうわけで通らないのです。
これの対処法として、デフォルト値以外を参照するように新たに設定する必要があります
それがこちら
has_many :active_relationships, class_name: "Relationship"
参照にするクラスの名前は「"Relationship"」です!とすればいいだけです
補足ですが、実際は投稿の方も
has_many :microposts, class_name: "Micropost"
となっています。ですが、デフォルトの挙動ということで省略されているだけの話です。
さて、お次はテーブル同士を紐づける作業です。
またまた投稿の時を参考に考えていきましょう
has_many :microposts
こう書かれているだけだったので、この紐付けの作業についてもデフォルトの挙動だったはずです。
実際紐づいていたのは、「Users」テーブルの「id」と「Microposts」テーブルの「user_id」です。
なるほどなるほど。この法則でいくと本来であれば
has_many :microposts, foreign_key: "user_id"
と書かれていたものが、省略されていたことが見えてきます。だってデフォルトだもん。
{モデル名}_idがデフォルトってことでしょう
※「foreign_key」とは自身のid(このケースで言えば「Users」テーブルの「id」)を指定したテーブル(このケースで言えば「Microposts」テーブル)のどのカラムと紐づけるかを決める
この法則で今回のケースで考えると、先ほどの書き方だと(デフォルトのままだと)「Users」テーブルの「id」と紐付けられるのは「Relationships」テーブルの「user_id」ということになってしまいます。そんなんありませんよね。あるのは「follower_id」と「followed_id」の二つです。
今回のケースでは当たり前ですが、「follower_id」が該当します。
よってこうすることで解決します
has_many :active_relationships, foreign_key: "follower_id"
結果こうなりました
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
あ、何も説明ありませんでしたが、しれっと「dependent: :destroy」が混じってます。
これは前回も登場していましたが、ユーザーを削除したらリレーションシップも同時に削除される必要があるため書かれてます。
見てるだけで息切れしてしまいそうですが、今度はリレーションシップ側から関連付けする必要があります。
「User」と「Micropost」の関係では「Micropost」は一人のユーザーに所属する「belongs_to :user」が使われていました。
「User」と「Relationship」の関係ではどうでしょうか。
ややこしいのは、「Relationship」に2つのパターンがあることです。
・誰をフォローしているか(follower)
・誰にフォローされているか(followed)
ですので、「belongs_to :user」としてひとつにまとめたいけどまとめられない事情があるため、このようになりました
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
「class_name: "User"」とわざわざ書いてあるのは先ほどのケースと同じ理由です。
こうすることで「has_many」と「belongs_to」の関連付けができました
これで思い出すのが便利なメソッドが使えるようになるということです。
micropostの時は
「micropost.user」や「user.microposts」などのメソッドの使用ができるようになりました。
今回は
「active_relationship.follower」や「active_relationship.followed」などのメソッドの使用が可能になります。
##14.1.3 Relationshipのバリデーション##
ここでRelationshipモデルの単体テストを入れます
フォローする側、フォローされる側、相互のidが存在することでフォロー機能が成立しますよね?という類のテストをまず書きます
内容はシンプルなので割愛します
テストが通るようにバリデーションを追加します
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
また、「rails generate」したときに、Relationship用のfixtureが作成されているはずです。こちらもとりあえず空にしときましょう
##14.1.4 フォローしているユーザー##
ユーザーが複数のactive_relationshipsを持っているところまで実装が終わりました。
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id"
「ユーザー → active_relationshipの"follower_id"」
ここの繋がりができたということです。
ですが、今後必要になってくるのはユーザーがフォローしているユーザーの情報(user.followed)です。
"follower_id"と対になっているのが"followed_id"です。
"follower_id"がフォローしているユーザーの「id」であり
"followed_id"がもちろんフォローされている方の「id」となります。
よって辿っていけばfollowedにたどり着くことはできます
「ユーザー → active_relationshipの"follower_id" → フォローされる人(user.followed)」
この説明については文字ではかなり限界があるので、テキストの図14.7を見ながら考えた方がよりわかりやすいかと思います
今のままでも、active_relationを辿ってuser.followedを引っ張ってくることも可能ですが、いかんせんコードが長くなりがちです。
例えばユーザーが最初にフォローした人を検索する場合
@user.active_relationships.first.followed
ユーザーがフォローした人全てを検索する場合
@user.active_relationships.map(&:followed)
と、コードも長くなりがちです。
ですので今回、もっと直接的にfollowedにたどり着く解決策としてこちらを準備します
has_many :followeds, through: :active_relationships
「through: :active_relationships」
これを読んだ通りに、「active_relationshipを通して」ということを表しているのかな・・・
なんて考えると失敗します!!!
また、問題になるのがこちら
「followeds」
まさに複数のフォローされてる人を表しているのですが、英語の世界では「user.followeds」という表現が不適切らしいです。
よってここは視点を変えて
「followed(ユーザーにフォローされてる人)」
ではなくて
「following(ユーザーがフォローしている人)」
という見方をするようにします。どちらも同じ人を指してますので。
結果、こんな風になります。
has_many :following, through: :active_relationships, source: :followed
これが意味しているのは、
**「following」メソッドとは、userクラスのインスタンス(誰が)に、フォローしている人を計上する「active_relationships」メソッドを実行し、そこで得られたインスタンスデータに対して「belongs_to」で定義したフォローされてる人を計上する「followed」メソッドを実行したもの。
繰り返しますが
「following」メソッドとは
「誰が」「どこに」フォローしているのかを表すメソッド。**ということです。
基本に戻ると、「:active_relationships」や「:followed」や「:destroy」などの「:〇〇」って「〇〇メソッド」を意味しますからね。
ややこしいところなので、とことん繰り返し考えていきます
まずは 「誰が」です
active_relationshipsはRelationshipテーブル内の「どのユーザーのデータを?」を抽出の役割があしました
次に 「どこに」です
followedメソッドを追加し、active_relationshipsが引っ張ってきた一覧からどこに対して、「どこのユーザーをフォローしているか?」を抽出できるようにします。
ここでは自分がフォローしているactive_relationships一覧から「相手方Userのfollowed(フォローされたやつ)」をつけると考えやすいかと思います。
ここでやっと「following」が「belongs_toとhas_manyの関連付けのおかげで使えるメソッド」となり
こうやって書いていたもの
@user.active_relationships.map(&:followed)
が、ここまでスッキリします
@user.following
こちらは以下のような感じて使います
user.following.include?(other_user)
⇨other_userはユーザーがフォローしている人に含まれますか?
user.following.find(other_user)
⇨other_userをユーザーがフォローしている人の中から見つける
user.following << other_user
⇨「<<」は配列の最後に加える演算子でした。要はother_userをユーザーのフォローに加えるということ
user.following.delete(other_user)
⇨そのままですね。other_userをフォローしているユーザーから消去します
こんな感じで、いつものように便利なメソッドができました
結局のところ**「has_many」や「belongs_to」ってメソッドを作るメソッドだったんです!**
これまで何回か使ってきましたが思い返すと全くもってその通りになってます。
has_many :microposts => user.micropostsメソッド
has_many :active_relationships => user.active_relationshipsメソッド
has_many :following => user.followingメソッド
これらのメソッドは全てuserクラスのインスタンスメソッドにくっつけて一覧出すよー的な働きをしています。
いや〜動画購入していて良かった〜
動画の説明がなければ全然違う方向に理解していました。
ではとりあえずfollowing関連のテストを書いていきましょう
(userモデルに追加した機能のテストなので「models/user_test.rb」に書くこと)
test "should follow and unfollow a user" do
# michaelさんをテストデータのmichaelさんとする
michael = users(:michael)
# archerさんをテストデータのarcherさんとする
archer = users(:archer)
# michaelさんはarcherさんをフォローしていませんよね?
assert_not michael.following?(archer)
# michaelさんがarcherさんをフォローします
michael.follow(archer)
# michaelさんはarcherさんをフォローしていますよね?
assert michael.following?(archer)
# michaelさんがarcherさんのフォローを解除します
michael.unfollow(archer)
# michaelさんはarcherさんをフォローしていませんよね?
assert_not michael.following?(archer)
end
しれっと登場しているメソッド「follow」「following」「unfollow」は定義する必要があります。
先ほど例で紹介したようなものばかりですが・・・
# ユーザーをフォローする
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
##14.1.5 フォロワー##
ここまでゴリゴリに時間をかけてきたので、ここはすんなりいきそうです。
フォローする側の組み立てが完成していますので、フォローされる側についてはそれをそのままそっくり逆の形で使うことができます。
フォローする側で頑張って作ったメソッド「following」に対して、フォローされる側では「followers」メソッドを作ることがゴール地点となります。
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
比較しやすくするために、フォローする側の情報も並べて載せました。
「active_relationship」→「passive_relationship」
「follower_id」→「followed_id」
「following」→「followers」
「followed」→「follower」
このように、対になっている部分を入れ替えるだけで機能としては問題なさそうです。
とりあえずこれで、「自分をフォローしている人たちの集合」を取得できるようになりました。
ここで定義した「followers」メソッドとは
自分がフォローされているpassive_relationships一覧から「相手方Userがfollowerとなっているもの(user.followerとしてフォローしたやつ)」をつけると考えやすいかと思います。
最後に、テストについても1行加えるだけです
michael.follow(archer)
assert michael.following?(archer)
# こいつを挿入 => archerさんのフォロワーにmichaelさんは入っていますか?
assert archer.followers.include?(michael)
#14.2 [Follow]のWebインターフェイス#
ここまでの苦労が報われる時がやってきました。
機能の搭載としては「モデル」の動作を組み立てたので、残りの「コントローラー」と「ビュー」の組み立てを行って完成に持っていきます。
##14.2.1 フォローのサンプルデータ##
フォローしている/されている関係をサンプルデータで作ります。
サンプルデータと言えばseedです。・
# 以下のリレーションシップを作成する
users = User.all
user = users.first
# ユーザー3〜51
following = users[2..50]
# ユーザー4〜41
followers = users[3..40]
# ユーザーが[2..50]分繰り返しフォローする
following.each { |followed| user.follow(followed) }
# ユーザーが[3..40]分繰り返しフォローされる
followers.each { |follower| follower.follow(user) }
これでユーザーid:3〜id:51をフォローし
ユーザーid:4〜id:41からフォローされるデータが完成します
仕上げは
$ rails db:migrate:reset
$ rails db:seed
##14.2.2 統計と[Folow]フォーム##
ここからは多数のパーシャルを作っては、はめ込んでを繰り返して「ビュー」を組み立てていきます
フォロー情報「フォロー数」「フォロワー数」を載せるのは、プロフィールページとHomeページです。
まずはルーティングから組み立てましょう
resources :users do
member do
get :following, :followers
# GET users/1/following
# GET users/1/follwers
end
end
ここにきて初めて見る表記方法です。。。
テキストも終盤ですが、「ルーティング」をおさらいします
まずはこちらが「resources :users」で追加されるアクションです
「index, show, new, edit, create, update, destroy」
これらは、これまで散々扱ってきたRailsのデフォルトのアクションです
ですが、今回フォローの機能を搭載するにあたってはデフォルトではない
「following, followed」
というアクションを使用します。さてどうしましょう
resources :users
これだけですと7つのアクションのルーティングができるだけですが、特定のアクションを追加したい場合は追加するアクションをmemberブロックまたはcollectionブロック内に記述します。
memberブロック
特定のデータを対象としているアクション
collectionブロック
全てのデータを対象としているアクション
書き方としてはこんな感じです
resources :リソース名 do
member do
HTTPリクエスト :特定のアクション名
end
end
resources :リソース名 do
collection do
HTTPリクエスト :特定のアクション名
end
end
これを今回のケースで当てはめてみますとこうなるわけですが
resources :users do
member do
get :following, :followers
end
end
リソース名 → usersリソース
memberブロック → 特定のデータを対象
HTTPリクエスト → GETリクエスト
特定のアクション名 → following,followers
となることが分かります。
これで7つのアクション名以外のアクションができました。というわけです
ちなみにそれぞれのURLとその後の動きについては
「users/:id/following」にアクセスすることで
'users#following' => usersリソースのfollowingアクションが動きます
「users/:id/followers」にアクセスすることで
'users#followers' => usersリソースのfollowersアクションが動きます
補足
collectionブロックにした場合はどんな感じなのか
resources :users do
collection do
get :search
end
end
とまあこんな感じですね。
動作としては
「users/search」にアクセスすることで
'users#search' => usersリソースのsearchアクションが動きます
何はともあれ、デフォルトではないアクション「following」「followers」とそれにアクセスするURLが完成しました。
ルーティングが完成しましたので「ビュー」作成に取り掛かります。
冒頭で説明した通りフォロー情報はページの一部に組み込むようにするため、パーシャルで部品のように作って、プロフィール画面や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>
1行目の<% @user ||= current_user %>
についてはもうおなじみですよね?
Rubyでは、|| 式を左から右に評価し、演算子の左の値が最初にtrueになった時点で処理を終了する
よってバラしたら当然こうなるわけですが
@user = @user || current_user
@user
がnilの場合はcurrent_userが入り、nilではない場合は何もせずそのまま@user
が入る。。ということです!!
他には
@user.following.count
@user.followers.count
これらは、カウント数出してるだけですよね。
フォロー統計情報のパーシャルができましたのでHome画面のいい感じのところに差し込みましょう
<section class="stats">
<%= render 'shared/stats' %>
</section>
ついでにCSSも整えときましょう。コピペで・・・
フォロー統計情報のパーシャルは各ユーザーのプロフィールページにも載せることは何度も言ってきましたが、そこには「follow」「unfollow」ボタンも表示させます。
そのボタンの意味は・・・考えたら分かるでしょ
とりあえずそのボタンのパーシャルを作成します
# 自分じゃなかったら? => 自分で自分をフォローしないようにするため
<% unless current_user?(@user) %>
<div id="follow_form">
# フォロー中だったら
<% if current_user.following?(@user) %>
# 「unfollow」が表示するようにします
<%= render 'unfollow' %>
# そうでなければ
<% else %>
# 「follow」が表示するようにします
<%= render 'follow' %>
<% end %>
</div>
<% end %>
ここでの@user
はフォロー対象のユーザーを指します
また、Relationshipsリソース用の新しいルーティングが必要になりますので作っときましょう
resources :relationships, only: [:create, :destroy]
なんでいきなりルーティング?となりましたがその理由はすぐに判明します
先ほどのボタンのパーシャルのパーシャルを作っていきます
まずはフォロー中ではなかった場合のパーシャル
<%= form_with(model: current_user.active_relationships.build, local: true) do |f| %>
# 対になっている相手方のidはユーザーにわざわざ見せなくてもいいからhiddenで隠している
<div><%= hidden_field_tag :followed_id, @user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
ざっくり言うと
POSTリクエストを Relationshipsコントローラに送信してリレーションシップをcreate(作成)しております
噛み砕いて言うと
「current_user.active_relationships」で自分がフォローする対象が集まっている集合に
「build」で、データを作成する準備をする
自分のidは分かっているので、あとはフォローしたい相手のidがわかればデータとして成立する。
ただ、その相手のidは現在いるプロフィールページから抜けばいいので、わざわざユーザーに入力してもらう必要はない。
必要だけど、わざわざ指定してもらう必要もないものはhiddenで見えないようにしておいて勝手に抜きます。
そこで、ボタンを押すことでcreateアクションが動いてフォローするために必要となるデータ(相手のid「followed_id」)をmodelに渡す(POSTリクエストを送る)。という流れ。
これまでは、アクションで作成していたインスタンス変数を、今回はビューの中で作成(build)しています。ですので、ここのビューで出てきている@user
はここで作成されたものということになります。
次はフォロー中だった場合のパーシャル
<%= form_with(model: current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete }, local: true) do |f| %>
<%= f.submit "Unfollow", class: "btn" %>
<% end %>
ざっくり言うと
DELETEリクエストを送信してリレーションシップをdestroy(削除)しています。
噛み砕いて言うと
「current_user.active_relationships」で自分がフォローする対象が集まっている集合の中から「find_by(followed_id: @user.id)」で対象を絞る。
そしてそのデータに対して、ボタンを押すことでdestroyアクションが動き、modelにDELETEリクエストを送る。という流れ。
これでいきなりルーティング挟んだわけがわかりましたね。
form_withを使ってRelationshipモデルオブジェクトをガンガン操作しています。
そして仕上げです
頑張って作ったパーシャルをプロフィールページに差し込んだら完成です
:
<section class="stats">
# 統計情報のパーシャル
<%= render 'shared/stats' %>
</section>
</aside>
<div class="col-md-8">
# フォロー/フォロー解除ボタンのパーシャル(ログインしていることを前提にする)
<%= render 'follow_form' if logged_in? %>
:
##14.2.3 [Following] と [Followers] ページ##
先ほどの節では、統計数のパーシャルを差し込みました。
こいつ 👉 「shared/_stats.html.erb」
そこには「フォローしている人 〇〇人」「フォローされてる人 〇〇」が表示されてますが、それぞれがリンクになってて、「フォローしている人一覧」と「フォローされている人一覧」を見ることができるようになってます。ってかこれからそれを作ります。。。
もちろんそんなページはログインしてないと見れないようにしておくべきなので、まずはテストから実装していきましょう
:
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
それぞれのページに進むための名前付きルートがこちら
following_user_path(@user):フォローしてるユーザー一覧
followers_user_path(@user):フォローされてるユーザー一覧
ここで苦労して作ったはいいがほったらかしになっていたアクションを使います
それがこいつ
resources :users do
member do
get :following, :followers
# GET users/1/following
# GET users/1/follwers
end
end
この「following」と「followers」ってアクション、特に何の定義もしてないので呼び出されたところで何の動作もしない状態のままほったらかしにされてました。。
今回はそいつらにやっと命を吹き込んであげます
まずは先ほどテストで書いた通り、ログインしてないと動かないメソッドとします
before_action :logged_in_user, only: [:index,:edit, :update, :destroy,
:following, :followers]
そしてそれぞれのアクションの中身がこちらです
# GETリクエストが /users/:id/following にきたとき
def following
@title = "Following"
@user = User.find(params[:id])
@users = @user.following.paginate(page: params[:page])
render 'show_follow'
end
# GETリクエストが /users/:id/followers にきたとき
def followers
@title = "Followers"
@user = User.find(params[:id])
@users = @user.followers.paginate(page: params[:page])
render 'show_follow'
end
これまでにないパターンの定義方法です。
え?@title
って何?
renderで呼び出すページ一緒ってどういうこと?
インスタンス変数名が全部同じ?
アクション名と同じ名前のメソッド入ってる・・・
特にごっちゃになって混乱しがちなのが、Userモデルの「has_many」で同じ名前の「following」メソッドや「followers」メソッドを定義しているところです。
とりあえず、もう一度「following」メソッドについて見てみると
「following」メソッドとは、userクラスのインスタンスに、ひとつ前で定義した「active_relationships」メソッドを実行し、そこで得られたインスタンスデータに対して「belongs_to」で定義した「followed」メソッドを実行したもの
とあります。
あああ、なるほど。
要するに「@user.following」とすることで、ユーザーがフォローしている人の集合体を得ることができるということです。
それに「paginate(page: params[:page])」をつけて30個ひとかたまりのページにする。
なるほどなるほど。
なぜか3行目から解決するという、変則的な解説になってしまった・・・
@title
とrenderで同じページ'show_follow'を呼び出している件については、呼び出し先のビューを見てみると分かりそうです。。。
これから作りますけど・・・
<% provide(:title, @title) %>
:
:
@title
については最初の1行で解決です。
タブのタイトルに「Following」「Followers」を入れるだけ
ここの山場は「同じページ'show_follow'を呼び出している件」でしょう
どこでこのページから呼び出されているかまで話を戻して、一つ一つ見ていきましょう
まずは「統計数のリンク」からこのページを呼び出すところから始めます
【following一覧を見たい場合】
1.統計数のfollowingリンク(<%= following_user_path(@user) %>)をポチる
2.GETリクエストが「/users/:id/following」に飛ぶ
3.followingアクションが動く
4.@title,@user,@users
にそれぞれ値が代入される
5.4を反映させた'show_follow'を表示する
【followers一覧を見たい場合】
1.統計数のfollowersリンク(<%= followers_user_path(@user) %>)をポチる
2.GETリクエストが「/users/:id/followers」に飛ぶ
3.followersアクションが動く
4.@title,@user,@users
にそれぞれ値が代入される
5.4を反映させた'show_follow'を表示する
んん〜なるほど。
出発地点が違うので、結果同じ画面でも表示される内容が違うことになるという話ですね。
今回はレイアウトが全く同じで、表示するデータの内容が違うというケースでしたのでインスタンス変数名も同じにしていたということです。
以上、同じテンプレートを使い回すテクニックでした。
最後に'show_follow'の全体像を提示しときましょう
<% 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">
# 「_user.html.erb」パーシャルを出力する
<%= render @users %>
</ul>
<%= will_paginate %>
<% end %>
</div>
</div>
さて、機能の搭載が完了しましたので統合テストを書いていきますか
今回は正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書きます
$ rails generate integration_test following
テストファイルができたら、今度はfixtureでテストデータを準備します
one:
follower: michael
followed: lana
two:
follower: michael
followed: malory
three:
follower: lana
followed: michael
four:
follower: archer
followed: michael
oneとかtwoとかはデータの「id」を指しています。
とりあえずテストの全体象を見てみましょう
def setup
# @userをmichaelさんとします
@user = users(:michael)
# ログイン状態とします
log_in_as(@user)
end
test "following page" do
# followingアクションにGETリクエストを送信
get following_user_path(@user)
# michaelさんがフォローしている人の集合体は空白ではありませんよね?
assert_not @user.following.empty?
# michaelさんがフォローしている人のカウント数が、ページ内の全てのHTML内に含まれてますよね
assert_match @user.following.count.to_s, response.body
# michaelさんがフォローしている人は、HTMLのリンクタグ(送信先はuser)で出力されてますよね
@user.following.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
test "followers page" do
# followersアクションにGETリクエストを送信
get followers_user_path(@user)
# michaelさんをフォローしている人の集合体は空白ではありませんよね?
assert_not @user.followers.empty?
# michaelさんをフォローしている人のカウント数が、ページ内の全てのHTML内に含まれてますよね
assert_match @user.followers.count.to_s, response.body
# michaelさんがフォローしている人は、HTMLのリンクタグ(送信先はuser)で出力されてますよね
@user.followers.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
特に、特別なものはなさそうですね。次、いきましょう。
##14.2.4 [Follow] ボタン (基本編)##
(基本編)とあるので、応用編もあるのでしょうか・・・
・・・それは置いといて
ここでは「Followボタン」と「Unfollowボタン」が動作するようにします
そういえば、ビューだけ先に作っといてほったらかしにしてましたね。。
まずはRelationshipsコントローラーから作成します(よう考えたら「モデル」と「ビュー」だけ先に作ってたな)
$ rails generate controller Relationships
まずは、先程と同様「Followボタン」と「Unfollowボタン」はログインしてないと動かせないようにすべきなのでテストから実装に入ります
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
ログインしてない状態で、「Followボタン」や「Unfollowボタン」押して、リクエスト送信してもデータベースのカウント数は変わりませんよー。という確認ですね。これは特に何ちゃないやつです。
ですのでちゃっちゃと次に進みましょう。
それでは本題に入ります。
「Followボタン」と「Unfollowボタン」でそれぞれ作っていたビューがこちら
<%= form_with(model: current_user.active_relationships.build, local: true) do |f| %>
<div><%= hidden_field_tag :followed_id, @user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
⇨ createアクションにPOSTリクエストを送ります
_unfollow.html.erb
<%= form_with(model: current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete }, local: true) do |f| %>
<%= f.submit "Unfollow", class: "btn" %>
<% end %>
⇨ destroyアクションにDELETEリクエストを送ります
という形になってますので、Relationshipsコントローラーには2つのアクションを作成します
```controller/relationships_controller.rb
# 全てのアクションがログインが前提で動きます
before_action :logged_in_user
# POST /relationships
def create
# フォローされる側のidを取得(hiddenで隠してたやつ)して「user」に入れます
@user = User.find(params[:followed_id])
# ログインしている前提のユーザーがフォローします
current_user.follow(user)
# フォローしたユーザーのプロフィールページに飛びます
redirect_to @user
end
# DELETE /relationships/:id
def destroy
# Relationshipsのデータは既に存在している状態
# フォームから送られてきたユーザーのidをフォローしたユーザーの中から取得して「user」に入れます
@user = Relationship.find(params[:id]).followed
# ログインしている前提のユーザーがフォローを解除します
current_user.unfollow(user)
# フォロー解除したユーザーのプロフィールページに飛びます
redirect_to @user
end
個人的に気になったのがここ
user = Relationship.find(params[:id]).followed
DELETEリクエストで送られてきたid(ここでは⭐︎とする)をRelationshipテーブルの中から見つけます。
(ただし、見つけるのはfollowersではなくてfollowedの方から)⇦ これが「followed」入れてる意味
この場合「user」はRelationshipテーブルのfollowed_idカラムに⭐️として載っているユーザーを指します。
この続きで、
current_user.unfollow(user)
とすることで、カラムの中でcurrent_userと⭐️が重なる部分がdestroyされるわけです。
なんか、しょーもないところで無駄に時間食っちゃいました・・・
##14.2.5 [Follow] ボタン(Ajax編)##
Ajax(アジャックス)とは
「Asynchronous JavaScript + XML」の略で、JavaScriptを使って非同期でサーバーとやり取りをする通信です
堅苦しい言い方はやめましょう。
これまでは「Follow」「Unfollow」ボタンを押すと、ページごと更新かけていました。
が、更新したい箇所は一部なのに、ページ全部更新かけるの無駄じゃね?ということで、
**「ページの一部だけ更新できるようにします!」**を、可能にしたのがAjaxです。
このAjax。初学者殺しなのが、RailsにJavaScriptを組み込んで実装していく流れになることです。
最終章まで全く登場しなかったのが、ここで新しく登場って・・・
ラストダンジョンで、急に全くレベル上げしてなかったメンバーで戦闘を強いられているような状況ですかね・・・
ま、愚痴ってもしょうがないのでさっさと取りかかりますか
ちなみにAjaxをヤバい分かりやすく説明してる記事がこちら 👉 初心者目線でAjaxの説明
「Follow」「Unfollow」ボタンに関わるところなので、ボタンのフォームを変更します
これまで使ってきたフォーム(form_with)についてですが、
form_with(model: ..., local: true)
最後に何気なくついていた「local: true」。
これって、まさかの「Ajax」使わない宣言だったんです!
ということで、使わない宣言を、使う宣言に変更します
form_with(model: ..., remote: true)
はい、これで完了!となりますので、「Follow」「Unfollow」ボタンのパーシャルを変更しましょう
<%= form_with(model: current_user.active_relationships.build, remote: true) do |f| %>
<%= form_with(model: current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete }, remote: true) do |f| %>
フォームの更新が終わりました。いわゆる「ビュー」の設定が終わった感じです。
お次は「コントローラー」にかかります
「Relationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう」なんてサラッと書いてますが、Ajaxリクエストとは?で、引っかかりました。
POSTとかGETなんかのHTTPリクエストみたいなニュアンスで捉えていて問題ないでしょうか・・・
とりあえず**「(remote: trueの状態で)フォームからコントローラーに送られる要求」**くらいに捉えときましょう
ここら辺から難しくなってきそうです・・・
ここでリクエストによって応答を場合分けする、respond_toメソッドの登場です。
これはどういうものかと言うと、今回「Follow」「Unfollow」ボタンからフォームを送信した場合はページの一部を更新する「Ajax」リクエストを送信しますが、他のリンクやフォームを選択した場合はもちろんページ全体を更新する「GET」や「POST」が送信されます。
この「Follow」「Unfollow」ボタンはいつもの違うリクエストが送られるので「respond_to」をつけて特別扱いしようということです。
前置き長くなりましたがこちらが「respond_to」は以下のような形で使用します
respond_to do |format|
format.html { redirect_to user } => 従来のリクエストの受付窓口
format.js => JavaScriptのリクエストの受付窓口
end
・・・一体何でしょうかこれ。。
説明によると、ブロック内のコードのうち、いずれかの1行が実行されるそうです。
format.html { redirect_to user }
か、もしくはこれが実行されない場合は
format.js
が実行されるということでいいのでしょうか
if文のようなものとありますが、なんでこれだけいきなりそうなるのか全く分かりません。
これはこういうものだから!と、暗記するしかないのでしょうか・・・まあ、特殊なメソッドだから仕方ありません。
さらに言えば
どちらかが実行されるとして、それぞれでどういう動きになるかも予想もつきません。これまでの学習がヒントになることもなければ何の説明もありません。
てことで自分で調べました
respond_toメソッド
リクエストで指定されたフォーマットによって処理を分ける事が出来るメソッドです
respond_to do |format|
# リクエストされるフォーマットがHTML形式の場合・・・①
format.html
# リクエストされるフォーマットがJavaScript形式の場合・・・②
format.js {render: @usres }
# @usersをJavaScript形式のデータへ変換して返す
end
①の場合、HTML形式の画面で返します
①のリクエストが来なかった場合②が実行され、JavaScript形式の画面で返します
表示形式が違えば、表示される画面の見た目もだいぶ変わってきますので、フォーマットごとに処理を分けたい場合に使用されるメソッドです
今回の場合ですと、「Follow」「Unfollow」ボタンから飛んでくるリクエストはAjax一択なので下の形式「format.js」が100%選ばれることになるんじゃね?って勝手に思ってますが・・・
まあ、とりあえずAjaxリクエストの受け取り手であるRelationshipsコントローラーのcreateアクションとdestroyアクションに「respond_to」メソッドを搭載しましょう
def create
# Ajaxの実装のため、インスタンス変数が必要になった
@user = User.find(params[:followed_id])
current_user.follow(@user) # => ここまでは同じ
# Ajaxリクエストに対応するため「respont_do」メソッドを使用
respond_to do |format|
format.html { redirect_to @user } # => HTMLをリクエストされたらHTMLで返す
format.js # => 特定のJavaScriptを実行する
# => app/views/relationships/create.js.erb
end
end
def destroy
# Ajaxの実装のため、インスタンス変数が必要になった
@user = Relationship.find(params[:id]).followed
current_user.unfollow(@user) # => ここまでは同じ
# Ajaxリクエストに対応するため「respont_do」メソッドを使用
respond_to do |format|
format.html { redirect_to @user } # => HTMLをリクエストされたらHTMLで返す
format.js # => 特定のJavaScriptを実行する
# => app/views/relationships/destroy.js.erb
end
end
シンプルなPOSTリクエストがきたら、いつものように単純に「@user(プロフィールページ)
」を返す
Ajaxでリクエストがきたら「JS-ERb(js.erb)」ファイルを返す
ということですね。
「コントローラー」への実装は以上です。
これでAjaxリクエストを受け取ることができるようになりました。
続いてはブラウザ側の対応の設定に入ります
Ajaxリクエストを受け取ることができても、ブラウザ側でJavaScriptが無効になっている場合もあるそうです。
じゃ使えねーじゃん。と思いますが、それでもAjaxリクエストを受け取る動きができるように設定できるそうです。以下がAjaxリクエストを受け取れるようにする設定
# 認証トークンをremoteフォームに埋め込む
config.action_view.embed_authenticity_token_in_remote_forms = true
Ajaxリクエストが呼び出すファイル形式
「respond_to」メソッドは、説明した通りAjaxリクエストを受けたら「JavaScript」用の埋め込みRubyファイルを呼び出します。
流れで見るとこんな感じ
HTML用の埋め込みHTMLファイル(html.erb)からAjaxリクエストを送信
↓
コントローラーの「respond_to」メソッドが処理
↓
いつもならhtml.erbが呼び出されていたけども、今回はRuby用の埋め込みRubyファイル(js.erb)が呼び出される
呼び出されるのは分かりましたが、そのようなファイルは準備していません。ないなら作りましょう。その**「JS-ERb(js.erb)」ファイル**とやらを
そもそもJS-ERbファイルの中身ってどうなってるの
ここにきて初登場の「JavaScript」用の埋め込みRubyファイルですが、これまでとは全然違う内容になります
まず、DOMとかいう仕組みを使ってページを操作しています。
しかもそのDOMですが、それ専用のDOM操作用メソッドが必要になります。
どういうメソッドかというと、jQueryライブラリ内にあるみたいです。
まあ、「JavaScript」用と言ってるくらいだから、中身はJavaScriptで書きましょうということですね。
こちらが「createアクション」と「destroyアクション」が呼び出す「JS-ERb(js.erb)」ファイルです
# 「Follow」「Unfollow」ボタンの切り替え
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
=> follow_formという「id」が振られているHTML要素を右の形に置き換えます
# 統計情報の更新
$("#followers").html('<%= @user.followers.count %>');
=> followersという「id」が振られているHTML要素を右の形に置き換えます
# 「Follow」「Unfollow」ボタンの切り替え
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
=> follow_formという「id」が振られているHTML要素を右の形に置き換えます
# 統計情報の更新
$("#followers").html('<%= @user.followers.count %>');
=> followersという「id」が振られているHTML要素を右の形に置き換えます
すっごいゴチャゴチャしてる・・・
全くもってこれまでになかったパターンの形式になってますね・・・
特徴的なのは**「$」記号で始まっていることや「#」シンボルが毎回使われている**ことです。
こんなのは、単純に記法なので覚えるしかないですけども。
まあ、jQueryの文法については他で勉強した方が良さそうです・・・
「JS-ERb(js.erb)」ファイル中身については、更新が必要な部分である、「Follow」「Unfollow」ボタンの切り替え・統計情報の更新のことしか記載されていません。
呼び出される部分が少なくなる分、更新も素早く完了することができるというわけです。
何はともあれ、ページの一部分だけデータ更新する機能はこれで完成しました。
##14.2.6 フォローをテストする##
後半かなり強引に終わらせましたが、一応フォローボタンが動くようになったのでテスト書きます
まあ、困ったのがいつもだったらPOSTリクエストで送信した場合の動作を見ていましたが、今回は・・・あの・・・「Ajaxリクエスト」送信が絡みます。。
POSTリクエスト送信ならこんな感じでよかったはずです
assert_difference '@user.following.count', 1 do
# POSTリクエストをcreateアクションに送信しまーす
post relationships_path, params: { followed_id: @other.id }
end
ここで朗報です。「Ajaxリクエスト」送信のテストでは、何とPOSTリクエストのテストに**「xhr :trueオプション」**をつけるだけで対応してくれるみたいです。最高!
assert_difference '@user.following.count', 1 do
# POSTリクエストをcreateアクションに送信しまーす
post relationships_path, params: { followed_id: @other.id }, xhr :true
end
これでいいみたいです。
なるほど!
てことで[Follow]/[Unfollow]ボタンの統合テストがこんな感じで仕上がってます
def setup
@user = users(:michael)
@other = users(:archer)
log_in_as(@user)
end
.
.
.
test "should follow a user the standard way" do
# 「followed_id」数は増えてますよね
assert_difference '@user.following.count', 1 do
# createアクションに@otherユーザーidをPOSTリクエストで送信します
post relationships_path, params: { followed_id: @other.id }
end
end
test "should follow a user with Ajax" do
# 「followed_id」数は1つ増えてますよね
assert_difference '@user.following.count', 1 do
# createアクションに@otherユーザーidをPOSTリクエストをAjaxリクエストで送信します
post relationships_path, xhr: true, params: { followed_id: @other.id }
end
end
test "should unfollow a user the standard way" do
# まずはarcherさんをフォローしときます
@user.follow(@other)
# 「relationship」にフォローされた人一覧にあるarcherさんのid入れときます
relationship = @user.active_relationships.find_by(followed_id: @other.id)
# 「followed_id」数は1つ減ってますよね
assert_difference '@user.following.count', -1 do
# michaelさんのフォロー一覧にあるarcherさんのid部分にDELETEリクエスト送信します
delete relationship_path(relationship)
end
end
test "should unfollow a user with Ajax" do
# まずはarcherさんをフォローしときます
@user.follow(@other)
# 「relationship」にフォローされた人一覧にあるarcherさんのid入れときます
relationship = @user.active_relationships.find_by(followed_id: @other.id)
# 「followed_id」数は1つ減ってますよね
assert_difference '@user.following.count', -1 do
# michaelさんのフォロー一覧にあるarcherさんのid部分にDELETEリクエストをAjaxリクエストで送信します
delete relationship_path(relationship), xhr: true
end
end
普通にPOSTリクエストで「フォロー」「フォロー解除」した場合とAjaxリクエストで「フォロー」「フォロー解除」した場合の2パターンでテストしてます。
さっきAjaxリクエストで対応するように機能実装したので、運用上ではPOSTリクエストのみで「フォロー」「フォロー解除」する機会があるのでしょうか・・・と思ったら、最初はHTML画面からのPOSTリクエストが送られ、それをcreateアクションがJavaScriptで返す。ということから始まってました。
#14.3 ステータスフィード#
ついにラスボスの登場となりました。
ラスボスだけあってテキストも「本書の中でも最も高度な内容」と、ハッキリ書いてあります。
ここで追加する機能というのがタイトルにもある「ステータスフィード」で
**自分のHOME画面に自分の投稿だけじゃなくて、フォローした人の投稿も載せる(しかも投稿順)**といった内容となってます
##14.3.1 動機と計画##
とりあえず
micropostsのuser_idカラムから自分がフォローしている人のものをピックアップして、自分のuser.feedに組み込む感じです。
機能の搭載よりテストの方が取り掛かりやすいのでまずはテストから書いていきます
テストで確認したい要素は3つ
①フォローしているユーザーの投稿がフィードに含まれていること
②自分自身の投稿もフィードに含まれていること
③フォローしていないユーザーの投稿がフィードに含まれていないこと
このテストをやる前に「michael」「archer」「lana」がそれぞれどういう関係性にあるかをfixtureで確認しときましょう
one:
follower: michael
followed: lana
two:
follower: michael
followed: malory
three:
follower: lana
followed: michael
four:
follower: archer
followed: michael
test "feed should have the right posts" do
michael = users(:michael)
archer = users(:archer)
lana = users(:lana)
# フォローしているユーザーの投稿を確認
# lanaさんの投稿
lana.microposts.each do |post_following| # =>ブロック変数の中身は分かればよい
# michaelさんのフィードに含まれてますよね?
assert michael.feed.include?(post_following)
end
# 自分自身の投稿を確認
# michaelさんの投稿
michael.microposts.each do |post_self| # =>ブロック変数の中身は分かればよい
# michaelさんのフィードに含まれていますよね?
assert michael.feed.include?(post_self)
end
# フォローしていないユーザーの投稿を確認
# archerさんの投稿
archer.microposts.each do |post_unfollowed| # =>ブロック変数の中身は分かればよい
# michaelさんのフィードに含まれていませんよね?
assert_not michael.feed.include?(post_unfollowed)
end
これが欲しい結果ですので、それに向けて組み立てていきましょう
##14.3.2 フィードを初めて実装する##
フィードといえばfeedメソッドです。
今回はそのメソッドを改良してデータベースから引っ張ってくる投稿が現在はユーザーの投稿のみとなっているところへ、フォローしているユーザーの投稿を加えます。
コード上では以下の内容となっていました。こいつをモデルチェンジして強化することが今回の目的ですね。
def feed
Micropost.where("user_id = ?", id)
end
ここの節で難易度を爆上げしている一つの要因がこれから扱うSQL文です。
SQL文でデータベースから今回は何の情報を引っ張ってくるんでしたっけ・・・
micropostテーブルから、自分がフォローしているユーザーidを持つ投稿を引っ張り出します
それをSQL文で表すとこうなります
SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>
Oh...Nanjakorya
一応こうすることでも同様のデータは引っ張ってくることはできます
current_user.microposts => 自分の投稿
current_user.following.map(:µposts) => フォローしてる人の投稿
こちらの方が目に優しいですが、ここは心を鬼にして茨の道を進みましょう
# micropost全体の集合の中から次の条件に合うものを全て取得してください
SELECT * FROM microposts
# user_idカラムにある情報が自分がフォローしているユーザーのidと一致している場合、もしくは自分自身のidと一致している場合
WHERE user_id IN (<list of ids>) OR user_id = <user id>
SELECT * FROM microposts
WHERE
この部分については
「Micropost.where」が表しています
それと自分がSQL勉強不足だったのですが、両方のデータを抜き出すなら「OR」が「AND」でもいいのでは?と思っていました
ですがそうなると「AND」の場合、自分がフォローしているユーザーのidであり、尚且つ自分自身のidでもあるidを抽出しようとします。そんなものはありませんよね。。てことで「OR」じゃないとだめです。
また、何と言ってもこれ
IN (<list of ids>)
= 自分がフォローしているユーザーのid
何でそうなるが、謎です。
あ、いや、これはあくまで「模式的」に表しているだけであって、そういうカチっとした意味があるわけではないです。ですのでIN (<list of ids>)
に自分がフォローしているユーザーのidを表すという意味はないです。
それでは「rails console」を使ってややこしい「WHERE」以降の部分の謎を紐解いていきましょう
@user = User.first
@user.following => フォローしているユーザーの一覧が出ます
@user.following.map(&:id) => フォローしているユーザーの「idの」一覧が出ます
おさらいしときましょう
mapメソッドについて(詳しくはこちら👉mapメソッドについて)
配列の要素の数だけ繰り返し処理を行うメソッドです。
上の例ですと、フォローしているユーザー一覧に一件一件「idは何でございやしょう」と、問い合わせを行っています。
とまあ、**データベースへの「id問い合わせ」**というものはrailsの世界では頻出しているらしく、もっと簡単に分かりやすく検索する方法が準備されてます。それがこちら
@user.following_ids(idの複数形?) => フォローしているユーザーの「idの」一覧が出ます
実はこのfollowing_idsメソッド、has_many :followingの関連付けをしたときにできたものらしいです。
これらのことを踏まえてfeedメソッドにフォローユーザー投稿表示機能を実装していきます、実装前と実装後を比較して見てみましょう
【実装前】
def feed
Micropost.where("user_id = ?", id)
end
たしか、「?」には「id」が入ってましたね。
【実装後】
def feed
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, self.id)
end
おおお。今度は?が2つになりました。
見た感じですと(?)に「following_ids」。2つ目の ? には「self.id」が入ればなんかしっくりきそうです。
##14.3.3 サブセレクト##
機能は実装は終わったのですが、修正点があるそうです。まじかよ。
現在データベースへの問い合わせの回数が無駄に多い点があります
1回目:following_idsでフォローしてる全てのユーザーをデータベースに問い合わせする
=> Relationshipsテーブルへの問い合わせ
2回目:Micropost.whereで該当のデータをデータベースに問い合わせする
=> Micropostsテーブルへの問い合わせ
これを1回にするのが今回の主旨です。
そのためにSQLのサブセレクトというSELECT文の中にSELECT文を入れるテクニックを使います。
まずは先ほどの修正したいコードを見ていきましょう
def feed
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, self.id)
end
これを修正して以下のようにします
def feed
Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
following_ids: following_ids, user_id: id)
end
単純に「?」がなくなりましたよね。
オプション引数で下に持っていってます。「?」が増えると単純に読みにくいですから。
「?」シンボルにして、それに対応するハッシュのキーとすることで、対応する部分に値が差し込まれます。
この時点ではまだ、データベースへの問い合わせは2回のままです。
following_idsはこう表すこともできます
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
まさにSELECT文の中にSELECT文が入ってます。
これでデータベースへの問い合わせが一つになるそうです。え、そうなの?
まあ ( ) 内のSELECT文を(Railsではなく)データベース内に保存するというところで違いが出てくるそうです。
そして仕上げがこちら
# ユーザーのステータスフィードを返す
def feed
# Relationshipsテーブルのfollowed_idカラムからfollower_idがユーザーのidになってるやつのデータを全て抽出してね
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
もはや「following_ids」は変数扱いになってますね。。。
今回の修正で、先ほどの
2回目:Micropost.whereで「user_id」であるデータをデータベースから取り寄せる
=> Micropostsテーブルへの問い合わせ
のみで処理を完結させることができるようになりました
以上で完成です
まあ、これについてはそんなに実用化されているテクニックではないみたいですので、頭の片隅に入れときます
何だかラスボスよりもフォロー機能搭載していたラストダンジョンの方が難易度高かったような気がしますが。。
#14.4 最後に#
終わった・・・ついに完走しました
チュートリアルRailsの主要な機能を学ぶことができました
モデル、ビュー、コントローラー、テンプレート、パーシャル、beforeフィルター、バリデーション、コールバック、has_many、belongs_to、has_many_through、関連付け、セキュリティ、テスティング、デプロイなどなど
ただし!!
ここはスタートラインでもあります。この機能だけで完結しているサイトなど存在しないからです。
##14.4.1 サンプルアプリケーションの機能を拡張する##
チュートリアルで実装した機能でもまだまだ一般的な機能はあります。(検索、返信、メッセージ)
今後これらの機能を拡張して、サンプルアプリケーションをパワーアップしたオリジナルのサイトを自分の力で作っていかなくてはなりません。
「Railsガイド」や「Google先生」からヒントを得ながら作成するのは、チュートリアルでやったどの章よりも難易度が高くなると思います・・・
テキストでは拡張機能として加える機能一覧が掲載されてます
##14.4.2 読み物ガイド##
今後学習を進めていくにあたっての、Railsに関連する書籍などが紹介されてます
#14章の要約をようやく作り終えての感想#
ようやく終わりました。。。
時間をかけすぎた感はありますが、自分の理解力ではこのペースが限界だったのでしょう。軽く4ヶ月くらいかかってます・・・
挫折せずにやりきれたのは、間違いなく「Railsチュートリアル解説動画」を見ながら進めたからです。
プログラミング初学者がチュートリアルに取り組むならマストで購入すべきです。
チュートリアルのサンプルアプリケーションを作成するのは実は2回目だったんですが、1回目は理解しないままどんどんコードだけ書き写すことを行い、理解度ゼロで形だけできるという悲惨な状況でした。
そこで2周目は理解度を高めるためにはやはりアウトプットが必要かと考え直して、この記事を書くことに決め、動画も購入し、バカ丁寧に、分からないところは分かるまでパソコンと睨めっこしながら取り組みました。
地道に取り組んでいたらいつかはゴールに辿り着きます。ダラダラやっちゃうのはよくないですがここをやり切らないとスタートラインにも立てないのも確かです。
今後はオリジナルサイト作成に取り組む予定です。