「フォロー」という関係性の特徴
今回のサンプルアプリケーションで使う「フォロー」という関係性には、以下のような特徴があります。
- 1人のユーザーは、0人以上の任意のユーザーからフォローされる
- 1人のユーザーは、0人以上の任意のユーザーをフォローする
こうした関係性は、データモデルの問題 (および解決策)でも触れたように「多対多」と呼ばれます。relationships
なるテーブルが必要になった理由も、フォロー関係が多対多の関係性であることが理由ですね。
Railsにおいて、あるテーブルから交差テーブルを介して多対多の関係性を取得するには
今回は、「has_many :through
という関連付けを使う」というという手段をとります(他には「has_and_belongs_to_many
という関連付けを使う」という手段もあります)。
has_many :through
関連付けの基本的な記法
Userモデルにおける以下のような記法が、has_many :through
関連付けの基本的な記法となります。
has_many :followeds, through: :active_relationships
上記のコードは、「followeds
という関係性に割り付けられた属性を、:active_relationships
関係性から参照する」という動作になります。第1引数は「Relationshipモデルに割り付けられた関係性」、:through
オプションは「Userモデルに割り付けられた、RelationshipモデルからUserモデルをどう参照するかの関係性」をシンボルで記述しています。
ただ、user.followeds
という名前は英語として不適切であるため、実際のアプリケーションでは使用しないこととします。user.following
という名前を使いたいです。さあどうするか。
関係性の名前と、対象テーブルにおけるRails暗黙の属性名が異なる場合のhas_many :through
関連付けの記述
関係性の名前が:followeds
であれば、relationship
テーブルのfollowed_id
という属性名を自動で参照してくることができます。しかし、関係性の名前を変える場合、relationship
テーブルの対応する属性名は明示的に与えなければなりません。このような場合、関連性を割り当てる対象の属性を明示的に与える必要があります。当該属性の名前を与える場所は、has_many :throughs
メソッドでは:source
オプションの値となります。
has_many :following, through: :active_relationships, source: :followed
以上の例では、「following
関係性の元はfollowed
idの集合である」ということを明示的に与えています。
Userモデルにfollowing
関連付けを追加する
以上の記述を踏まえると、Userモデルの実体である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
...略
end
Userモデルにfollowing
関連付けを追加すると、結果として何ができるようになるのか
以下のような操作を行えるようになります。
- フォローしているユーザーの集合を調べる
- 関連付けを通して、フォローしている特定のユーザーを検索する
- 配列と同様にして、フォローしているユーザーを新規追加する
- 特定のユーザーをフォローから削除する
>> user = User.first
>> user.following.include?(User.second)
=> false
>> user.following << User.second
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, ...>]>
>> user.following.include?(User.second)
=> true
>> user.following.delete(User.second)
=> [#<User id: 2, ...>]
>> user.following.include?(User.second)
=> false
上記のirbのログは、以下のような順番の処理を実行しています。
- (
user.following.include?(User.second)
id=1のユーザーが、id=2のユーザーをフォローしているかどうかを取得する - (
user.following << User.second
)id=1のユーザーが、id=2のユーザーをフォローする - (
user.following.include?(User.second)
)id=1のユーザーが、id=2のユーザーをフォローしているかどうかを取得する - (
user.following.delete(User.second)
)id=1のユーザーが、id=2のユーザーのフォローを解除する - (
user.following.include?(User.second)
)id=1のユーザーが、id=2のユーザーをフォローしているかどうかを取得する
Railsは、フォローしているユーザーの集合を最適な形で扱う
「following
オブジェクトは、フォローしているユーザーの集合を配列のように扱うことができる」と言及しました。しかしそれだけではありません。「Rubyで処理するよりSQLで処理するほうが高速な処理は、SQLでやらせるように最適化する」という処理も自動で行っているのです1。
>> user.following
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, ...>, #<User id: 42, ...>]>
>> user.following.count
(0.6ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]]
「RDBMS側で処理を行っている」ということを説明する上で、SQL文中におけるINNER JOIN
というキーワードが大きなポイントになります。「users
テーブル全体ではなく、users
テーブルの一部分のみを抽出する」という処理を行うようなSQLが発行されているのですね。具体的には、以下のような処理がRDBMS中でシームレスに行われています(following
メソッドの実行結果として、relationships
テーブルの内容は戻ってきません)。
-
relathinships
テーブルのfollower_id
が、following
メソッドを呼び出したUserオブジェクトのid
と等しいレコードのみを取り出す(この結果そのものは使わない) -
users
テーブルから、idが
1.で取り出されたレコードのfollowed_id
と等しいレコードのみを取り出す
followingオブジェクトに関する追加のメソッドを実装する
followingオブジェクトが使えるようになったら、ユーザーのフォローやアンフォローといった処理をより直感的な記述で行えるようにするためのメソッドを実装していきましょう。例えば、「user.follow(other_user)
というメソッドの呼び出しでユーザーをフォローできる」というような形にしていくのです。
followingオブジェクトに関する追加のメソッドに対するテストを記述する
followingオブジェクトに関する追加のメソッドは、テスト駆動の形で実装していきます。以下の理由により、テスト駆動のほうが新たに実装するメソッドの動作内容をイメージしやすいためです。
- Webインターフェース等で
follow
やunfollow
といったメソッドを使うのはまだ先である- 必要なビューやコントローラーの実装はまだされていない
- 一方で、Userモデルを対象としたテストであれば直ちに行える実装が完成している
require 'test_helper'
class UserTest < ActiveSupport::TestCase
...略
+
+ test "should follow and unfollow a user" do
+ rhakurei = users(:rhakurei)
+ mkirisame = users(:mkirisame)
+ assert_not rhakurei.following?(mkirisame)
+ rhakurei.follow(mkirisame)
+ assert rhakurei.following?(mkirisame)
+ rhakurei.unfollow(mkirisame)
+ assert_not rhakurei.following?(mkirisame)
+ end
end
動作は以下の通りです。
- 最初の時点で
following?
メソッドを実行し、フォロー状態でないことを確認する -
follow
メソッドを実行する -
following?
メソッドを実行し、フォロー状態であることを確認する -
unfollow
メソッドを実行する -
following?
メソッドを実行し、フォロー状態でないことを確認する
この時点では、当然ながら上記のテストは通りません。
# rails test test/models/user_test.rb
ERROR["test_should_follow_and_unfollow_a_user", UserTest, 1.2591099999990547]
test_should_follow_and_unfollow_a_user#UserTest (1.26s)
NoMethodError: NoMethodError: undefined method `following?' for #<User:0x0000560eb3173a50>
Did you mean? following
following=
test/models/user_test.rb:89:in `block in <class:UserTest>'
14/14: [=================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.29720s
14 tests, 22 assertions, 0 failures, 1 errors, 0 skips
そもそもfollowing?
メソッドそのものがないですよね。はい。
followingオブジェクトに関する追加のメソッドを実際に実装する
app/models/user.rb
に、必要なメソッドを追加していきます。
class User < ApplicationRecord
...略
def feed
...略
end
+
+ # ユーザーをフォローする
+ 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
...略
end
上記のコードを全て実装すると、テストは成功するようになります。
# rails test
Running via Spring preloader in process 270
Started with run options --seed 28572
65/65: [=================================] 100% Time: 00:00:16, Time: 00:00:16
Finished in 16.73850s
65 tests, 337 assertions, 0 failures, 0 errors, 0 skips
-
一般に、「RDB内における、特定の条件に合致するレコードを抽出する」というような処理は、アプリケーション側で行うよりもRDBMS側で行うほうが高速です。RDBMSにおいて、RDBに対する処理は高度に最適化されているためです。 ↩