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

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

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

* 1人のユーザーは、0人以上の任意のユーザーからフォローされる
* 1人のユーザーは、0人以上の任意のユーザーをフォローする

こうした関係性は、データモデルの問題 (および解決策)でも触れたように「多対多」と呼ばれます。`relationships`なるテーブルが必要になった理由も、フォロー関係が多対多の関係性であることが理由ですね。

# Railsにおいて、あるテーブルから交差テーブルを介して多対多の関係性を取得するには

今回は、「`has_many :through`という関連付けを使う」というという手段をとります（他には「`has_and_belongs_to_many`という関連付けを使う」という手段もあります）。

## `has_many :through`関連付けの基本的な記法

Userモデルにおける以下のような記法が、`has_many :through`関連付けの基本的な記法となります。

```ruby:
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`オプションの値となります。

```ruby:
has_many :following, through: :active_relationships, source: :followed
```

以上の例では、「`following`関係性の元は`followed` idの集合である」ということを明示的に与えています。

# Userモデルに`following`関連付けを追加する

以上の記述を踏まえると、Userモデルの実体である`app/models/user.rb`に追加するコードは以下の通りになります。

```diff: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でやらせるように最適化する」という処理も自動で行っているのです[^SQLで処理するほうが高速な処理]。

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

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

```diff: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`に、必要なメソッドを追加していきます。

```diff: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
```