0
0

More than 3 years have passed since last update.

Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデル - フォローしているユーザー

Posted at

「フォロー」という関係性の特徴

今回のサンプルアプリケーションで使う「フォロー」という関係性には、以下のような特徴があります。

  • 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に追加するコードは以下の通りになります。

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のログは、以下のような順番の処理を実行しています。

  1. (user.following.include?(User.second)id=1のユーザーが、id=2のユーザーをフォローしているかどうかを取得する
  2. (user.following << User.second)id=1のユーザーが、id=2のユーザーをフォローする
  3. (user.following.include?(User.second))id=1のユーザーが、id=2のユーザーをフォローしているかどうかを取得する
  4. (user.following.delete(User.second))id=1のユーザーが、id=2のユーザーのフォローを解除する
  5. (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テーブルの内容は戻ってきません)。

  1. relathinshipsテーブルのfollower_idが、followingメソッドを呼び出したUserオブジェクトのidと等しいレコードのみを取り出す(この結果そのものは使わない)
  2. usersテーブルから、idが1.で取り出されたレコードのfollowed_idと等しいレコードのみを取り出す

followingオブジェクトに関する追加のメソッドを実装する

followingオブジェクトが使えるようになったら、ユーザーのフォローやアンフォローといった処理をより直感的な記述で行えるようにするためのメソッドを実装していきましょう。例えば、「user.follow(other_user)というメソッドの呼び出しでユーザーをフォローできる」というような形にしていくのです。

followingオブジェクトに関する追加のメソッドに対するテストを記述する

followingオブジェクトに関する追加のメソッドは、テスト駆動の形で実装していきます。以下の理由により、テスト駆動のほうが新たに実装するメソッドの動作内容をイメージしやすいためです。

  • Webインターフェース等でfollowunfollowといったメソッドを使うのはまだ先である
    • 必要なビューやコントローラーの実装はまだされていない
  • 一方で、Userモデルを対象としたテストであれば直ちに行える実装が完成している
test/models/user_test.rb
  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

動作は以下の通りです。

  1. 最初の時点でfollowing?メソッドを実行し、フォロー状態でないことを確認する
  2. followメソッドを実行する
  3. following?メソッドを実行し、フォロー状態であることを確認する
  4. unfollowメソッドを実行する
  5. 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に、必要なメソッドを追加していきます。

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

  1. 一般に、「RDB内における、特定の条件に合致するレコードを抽出する」というような処理は、アプリケーション側で行うよりもRDBMS側で行うほうが高速です。RDBMSにおいて、RDBに対する処理は高度に最適化されているためです。 

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0