65日目
今日の学習
Ruby on Rails
Railsチュートリアル 第14章
ついに最終章となった。ユーザーごとに投稿ができる機能などの実装を行ったことはあるものの、ユーザーをフォローするという機能を作成するのは初めてなので楽しみ。
Relationshipモデル
データモデルの作成
ユーザーをフォローするデータモデル構成で想定される一般的なケースを考えてみる。
Railsの複数形の慣習に従うと、「あるユーザーをフォローしている全てのユーザー」の集合はfollowers
という名前になり、
user.followers
で、フォロワーの配列を呼び出すことができる。
しかし、この慣習に従えば、その逆の「あるユーザーがフォローしているすべてのユーザー」の集合はfolloweds
という名前になる。これは英語の文法から外れており、意味も分かりにくい。
そのため、Twitterの監修にならって、「あるユーザーがフォローしているすべてのユーザー」に対してfollowing
という呼称を採用する。
そのため、例えばhogeさんがフォローしている全てのユーザーを呼び出したい場合は、
hoge.following
という形になる。
名称を決定したので、following
テーブルとhas_many
で関連付けを行い、フォローしているユーザーのモデリングができる。
ユーザーの集合であるfollowing
テーブルのそれぞれの行には、followed_id
というユーザーを識別するカラムが必要となり、さらにユーザーの名前やパスワードなども必要になる。
例:userのidが1のhogeさんが、idが2, 5, 10のユーザーをフォローしている場合
user
テーブル id1のデータ
follower_id | name | password | |
---|---|---|---|
1 | hoge | hoge@email.com | hogehoge |
following
テーブル
follower_id | followed_id | name | password | |
---|---|---|---|---|
1 | 2 | fuga | fuga@email.com | fugafuga |
1 | 5 | ... | ... | ... |
1 | 10 | ... | ... | ... |
follower_id
は、hogeさんのidが登録される。一人のユーザーが複数持てるため、has_many
で関連付けられる。
followed_id
には、フォローされている人のidが記述されており、それ以降のnameやemailといった情報は該当idの情報が入る。
このデータモデルの問題点は、user
モデルには既にemailやpasswordが格納されているにもかかわらず、followingにも同じカラムがあり非常に無駄があるところ。
また、followers
のモデリングを作る際も、following
と同じようにnameやemailが格納されることになる。
もしも、誰かがユーザー名などを変更すれば、users
テーブルのレコードだけではなく、following
とfollowers
テーブルのレコード全ての行を更新することになり、負荷も多い。
これを解決するために、フォローの動作をもう一度確認する。
- あるユーザーが別のユーザーをフォローするときに、作成されるものはなにか
- あるユーザーが別のユーザーのフォローを解除するときに、何が削除されるのか
フォローという動作で作成・削除されるものは、ユーザー間の「関係(realationship)」であることが分かる。(フォローしている関係、フォローされている関係)
さらに、フォローという機能は左右非対称の性質がある。一般的なフレンド機能とは異なり、hogeさんがfugaさんをフォローしていたとしても、fugaさんがhogeさんをフォローしているとは限らない。
Railsチュートリアルでは、このような関係を以下のように読んでいる。
- 能動的関係(
Active Relationship
)
hogeさんから見たfugaさんとの関係(フォローしている) - 受動的関係(
Passive Relationship
)
fugaさんから見たhogeさんとの関係(フォローされている)
まずはフォローしているユーザーを生成するために、能動的関係に焦点を当てる。
フォローしているユーザーをfollowed_id
で識別することにし、following
テーブルをactive_relationship
テーブルに置き換えて考える。
users
テーブルと重複している、nameやemailのユーザー情報は無駄なので、ユーザーのid以外の情報は消してしまう。
followed_id
を通じて、users
テーブルのユーザーを見つけるようにする。
先ほどと同じように、ユーザーのidが1の人が、id2, 5, 10の人をフォローしている場合は以下のように表すことができる。
active_relationships
テーブル
follower_id | followed_id |
---|---|
1 | 2 |
1 | 5 |
1 | 10 |
followed_id
と一致しているusers
テーブルのユーザーidを探し出し、フォローしているユーザーを取得できる。(has_many
の関係)
relationships
データモデルを作成し、カラムにfollower_id
とfollowed_id
(データ型はどちらもinteger)を加える。
この二つのカラムは、今後頻繁に検索することになるため、検索の効率を上げるためにインデックスを追加する。
また、follower_id
とfollowed_id
の組み合わせが必ずユニークであるように設定し、同じユーザーを二回以上フォローすることを防ぐ。
add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, []follower_id, :followed_id], unique: true
User/Relationshipの関連付け
1人のユーザーにはhas_many
(1対多)の関係(relationship)があり、この関係は2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属する(belongs_to
)。
前回、User
とMicropost
を関連付けたときのようにして、新しいリレーションシップを作成する。
user.active_relationships.build(followed_id: ...)
このコードを実行するために、has_many
で関連づける必要があるが、今回のケースでは書き方を変えないとRelationship
モデルを探すことができない。
# 前回は引数のシンボルからMicropostモデルを探すことができた class User < ApplicationRecord has_many :microposts end # user_id属性をたどってユーザーを特定できる class Micropost < ApplicationRecord belongs_to :user end
データベースの2つのテーブルを繋いでくれるidを外部キー(foreign key)と呼ぶ。
マイクロポストの例で言うと、User
モデルに繋げる外部キーが、Micropost
モデルのuser_id
属性になる。
Railsはこの外部キーの名前を使い、関連付けの推測をしてくれる。
<class>_id
のように推測をしてくれるため、上のコードの場合はuser_id
となる。
しかし、今回はフォローしているユーザーをfollower_id
という外部キーを使って特定する必要がある上に、follower
というクラス名が存在しないため、Railsに正しいクラス名を伝える必要がある。
モデル名と関係名が異なるため、class_name
、foreign_key
を利用し、指定する。
app/models/user.rb
has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
能動的関係に対して、1対多の関連付けを行う。
ユーザーを削除したら、ユーザーのリレーションシップも同時に削除してほしいためdependent: :destroy
を付け加えてある。
次に、フォロワーに対してbelongs_to
の関連付けを行う。また、ついでにバリデーションも追加しておく。
app/models/relationship.rb
belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true
二つの関連付けにより、以下のメソッドが使えるようになった。
active_relationship.follower
フォロワーを返すactive_relationship.followed
フォローしているユーザーを返すuser.active_relationship.create(followed_id: other_user.id){<br>
user`と紐づけて能動的関係を作成するuser.active_relationship.build(followed_id: other_user.id)
user
と紐づけた新しいRelationshipオブジェクトを返す
has_many through
following
とfollowers
を結びつけるにあたって、has_many through
を使って関連付けを行う。
1人のユーザーには、いくつもの「フォローする」「フォローされる」といった関係がある。これは1対多ではなく、多対多の関係。
Railsはモデル名(単数)に対応する外部キーを探す。
has_many :followeds, through: :active_relationships
Railsはfolloweds
というシンボル名を見て、followed
という単数系に変えてrelationships
テーブルのfollowed_id
を使い、対象のユーザーを取得する。
しかし、followeds
という英単語は不自然なので、following
という名前を利用したい。
そのために、Railsのデフォルト設定であるモデル名(単数)に対応する外部キーを探すという部分を、:source
パラメータを使って変更する。
following
配列の元はfollowed
idの集合だということをRailsに伝えたいため、以下のようにする。
has_many :following, through: :active_relationships, source: :followed
関連付けを行ったため、フォローしているユーザーを配列のように扱えるようになった。
以下のように、メソッドを利用してオブジェクトを探すことができる。
user.following.include?(other_user) user.following.find(other_user)
following
で取得したオブジェクトは、配列のように要素を追加したり削除したりすることができる。とても簡潔かつ、分かりやすい。
user.following << other_user user.following.delete(other_user)
follow関連のメソッドの追加
関連付けによって集合を簡単に取得できるようになった。次は、これをより簡単に扱うために、follow
やunfollow
といった便利メソッドを追加していく。
メソッドの実装にあたり、テストを先に書き起こした。このメソッドを実際に使えるのはまだ先であるため、しっかり効用があるかどうかを事前に確認しておく必要がある。
今回用意するメソッドは以下の三種類。
- ユーザーをフォローする
follow
- ユーザーのフォローを解除する
unfollow
- あるユーザーが誰かをフォローしているかどうか確認する
following?
さらに、上記のメソッドを利用し、以下の流れでテストを行う。
following?
メソッドであるユーザーをまだフォローしていないことを確認follow
メソッドでそのユーザーをフォローfollowing?
メソッドでフォローしていることを確認unfollow
メソッドでフォローを解除following?
メソッドでフォローしていないことを確認
test/models/user_test.rb
test "should follow and unfollow a user" do # fixtureを使ってユーザーを設定する user1 = users(:user1) user2 = users(:user2) assert_not user1.following?(user2) user1.follow(user2) assert user1.following?(user2) user1.unfollow(user2) assert_not user.following?(user2) end
テストの準備ができたので、実際にメソッドを登録していく。user自身を表すself
が省略されている。
app/models/user.rb
def follow(other_user) following << other_user end def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end def following?(other_user) following.include?(other_user) end
フォロワー
今度は、user.following
と対になる、user.followers
メソッドを追加したい。
relationships
テーブルには、既にフォロワーを取得するために必要な情報が含まれているため、active_relationships
テーブルを再利用する。
app/models/user.rb
has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :followers, through: :passive_relationships, source: :follower
能動的関係を使った時とは逆になるように、受動的関係を使ってuser.followers
を実装する。
なお、本来followers
を指定するためにsource
を使用する必要はない。Railsはfollowers
の単数系であるfollower
を外部キーとしてそのまま探してくれる。
必要はないが、あえて書き足すことでhas_many :following
と対になっている類似性を強調してある。
これで、followers.include?
やuser.followers.count
といったメソッドが利用できるようになった。
count
などでDBを参照する場合、DBから直接算出しているため、処理が高速になるそうだ。
今日のやらかし
テストが実行できない
Relationshipモデルのバリデーションテストを行っていたが、エラーが出てしまう。
`test': test_should_require_a_follower_id is already defined in RelationshipTest (RuntimeError)
内容はチュートリアル通りで問題ないはずだが...と思い、チュートリアルのお手本をコピペしてみると、テスト名が重複していたことに気づく。
test "should require a follower_id" do test "should require a followed_id" do
上のテスト名が正しいが、誤ってどちらもfollowed_id
と書いてしまっており、テスト名が重複しているのが原因でエラーが出ていた。
already defined
で、「すでに定義されています」という意味になるが、テスト名でミスしているとは思わず内容が重複してしまっているかを確認してしまっていたので反省。