Railsチュートリアル 第14章 ユーザーをフォローする
いよいよ、最終章です! 完走ですね。
ただ、今回はより高度なモデリングを扱うようで、難易度高めみたいなので、気を抜かずに理解していきたいと思います。
フォロー機構の概要
今回はフォローとフォロワー機能を追加します。
自分がフォローしている相手はfollowing、
自分がフォローされている相手はfollowersと今回では表現します。
userモデルに対して、フォローしている相手情報のfollowingとフォローされている相手情報をfollowersをそれぞれ単体のモデルで表現できればいいのですが、無駄が多く、相手のユーザーがデータベースから削除された場合のことなどを考えると保守性が最悪なので、2ユーザーの間の関係性を表現したモデルを使います。
今回のTwitter形式のフォローとフォロワー機能の場合、能動的関係と受動的関係があります。
CalvinがHobbesをフォローしているが、HobbesはCalvinをフォローしていない場合では、CalvinはHobbesに対して「能動的関係」を持っていることになります。逆に、HobbesはCalvinに対して「受動的関係」と表現されます。
まず、能動的関係(cavinがhobbesをフォローしている)をどう表現するのかというと
フォローしている主体のuserモデル(id,name.email....)と、そのuserにフォローされているuserモデル情報を持つuser.following(id,name.email)の間に、
両者のid情報を持つ、active_relationships(id,follower_id,followed_id)と言うモデルを挟みます。
フォローが発生すると、このactive_relationshipsにデータが作成され、あるuserと、フォローされているuserとの関係性情報を生成します。
また、受動的関係(HobbesはCalvinにフォローされている)でも同じモデルを使い回すため、relationshipsモデルとして作成し、能動関係を記述する時はactive_relationshipsとします。
少し混乱するので整理すると以下の関係性になります。
フォローする人
userモデル
↓
関係性を記述したモデル
active_relationships(Relation)モデル
↓
フォローされる人
user.following モデル
リスト 14.1: relationshipsテーブルにインデックスを追加する
db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.0] 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 #同じ:follower_id,と:followed_idの組み合わせが2以上出ないようにunique:trueとする end end
14.1.2 User/Relationshipの関連付け
復習
- new
モデルオブジェクトを生成する。インスタンスを生成するだけで,保存はされていないためsaveメソッドなどを使用して保存する.
- build
newとほぼ同じ。自動的にuser_idをセットしてインスタンスを生成することができる。
- create
モデルオブジェクトを生成して保存までする.
14.2章までの設計で使えるようになったメソッド
メソッド | 用途 |
---|---|
active_relationship.follower | フォロワーを返します |
active_relationship.followed | フォローしているユーザーを返します |
user.active_relationships.create(followed_id: other_user.id) | userと紐付けて能動的関係を作成/登録する |
user.active_relationships.create!(followed_id: other_user.id) | userを紐付けて能動的関係を作成/登録する (失敗時にエラーを出力) |
user.active_relationships.build(followed_id: other_user.id) | userと紐付けた新しいRelationshipオブジェクトを返す |
14.1.4 フォローしているユーザー
これまでやってきたのはUserとRlationshipの関連付けですが、いよいよfollowingとfollowerに取り掛かります。
リスト 14.8: Userモデルにfollowingの関連付けを追加する app/models/user.rb
class User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed #User→active_relationships→followingという関連付けをする . . . end
source:followedは:sourceパラメーターを使って、「following配列の元はfollowed idの集合である」ということを明示的にRailsに伝える役割です。
userモデルに以上の記述をしたことにより、user.followingメソッドが使えるようになり、
上のactive_relationship.followedにUserとの関係性を包めた記述ができるようになりました。
メソッド | 用途 |
---|---|
user.following | userがフォローをしたユーザーデータの集団を取り出す |
少し混乱しましたので、「14.1.4 フォローしているユーザー」でやったことを整理すると、
userモデルをthroughでactive_relationshipsモデルを関連付けた、followingモデルをuser.rb内に作成したので、user.followingでuserがフォローをしたユーザーの情報が取り出せるようになりました。
14.2.5 [Follow] ボタン (Ajax編)
初めて出てきました技術要素Ajax。 再読込させること無く、画面の表示を動的に変更させるために使われます。
使い方は まず、html内の送信フォームでのコード対応としては
form_for
こういったコードがあるとすると
form_for ..., remote: true
とremote:trueを追加します
リスト 14.34: Ajaxを使ったフォローフォーム app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>
コントローラ側での対応は htmlでリクエストがあった場合と.Ajaxだった場合を書き以下になります。
respond_to do |format| format.html { redirect_to @user } format.js end
そして、アクセスが終わった後、Ajaxの場合画面を描画しなくてはならないので、
そのコントローラに対応したフォルダに.js.erb形式で
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); #follow_form内のhtmlをusers/unfollowに書き換える $("#followers").html('<%= @user.followers.count %>'); #followers内のhtml要素を@user.followers.countに書き換える
と書きます。
14.3.3 サブセレクト
ステータスフィード、つまりHomeページに表示されるフォローしている人の投稿画面ですが、以下のように実装します
リスト 14.44: とりあえず動くフィードの実装 green app/models/user.rb
# ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) #user.feedでフォローしている人のmicropostが取り出せる end
しかし、上の実装だと
following_idsでフォローしているすべてのユーザーをデータベースに問い合わせし、
さらに、フォローしているユーザーの完全な配列を作るために再度データベースに問い合わせしている非効率なモデルの検索方法となっているため、
フォローするユーザーが5000人を超すと動作が重くなってしまいます、
より効率的な書き方は以下です
# ユーザーのステータスフィードを返す def feed following_ids = "SELECT followed_id FROM relationships #ユーザー1がフォローしているユーザーすべてを選択するSQL文 WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids}) #上記のSQL文をSQL文の中に内包する OR user_id = :user_id", user_id: id) end
先程はrailsのコードとして、following_idsのデータの集団をデータベースから取得し、
それをもとに更にデータベースから集団を探して配列を作って、何度もデータベースへの問い合わせをしていましたが、
改良されたコードでは、SQLで母集団を作り、それをそのままSQL内で操作していくのでデータベース内で操作が完結でき、速度が早いと理解したけど、合ってるかな?
こういった、入れ子になった SELECT文をサブセレクトと言います。
14章のまとめ
has_many :throughを使うと、複雑なデータ関係をモデリングできる
has_manyメソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる
適切なクラス名と外部キーと一緒にhas_many/has_many :throughを使うことで、能動的関係 (フォローする) や受動的関係 (フォローされる) がモデリングできた
ルーティングは、ネストさせて使うことができる
whereメソッドを使うと、柔軟で強力なデータベースへの問い合わせが作成できる
ようやくRailsチュートリアル一周できました! だいたい一月弱ぐらい掛かかりましたね。 やっぱり高度なことを学習し覚えるのはすごく楽しい!!
ですが、その覚えたことを元に、オリジナルな何かを0から作り上げることはもっと好きです。
忘却曲線的にも内容を忘れないようにオリジナルの成果物を作って、より学習内容を定着させたいと思います。
次回は成果物に取り掛かっていきたいと思います