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

# データモデルの問題 (および解決策)

別記事で解説します。

* [Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデルにおけるデータモデルの問題 (および解決策)](https://qiita.com/rapidliner00/items/f543e266b034070ff752)

# 演習 - データモデルの問題 (および解決策)

## 1. [図 14.7](https://railstutorial.jp/chapters/following_users?version=5.1#fig-user_has_many_following)のid=1のユーザーに対して`user.following.map(&:id)`を実行すると、結果はどのようになるでしょうか? 想像してみてください。

> ヒント: [4.3.2](https://railstutorial.jp/chapters/rails_flavored_ruby?version=5.1#sec-blocks)で紹介した`map(&:method_name)`のパターンを思い出してください。例えば`user.following.map(&:id)`の場合、idの配列を返します。

```
>> user = User.find(1)
>> user.following.map(&:id)
=> [2, 7, 10, 8]
```

このようになるのではないでしょうか。

## 2.1. [図 14.7](https://railstutorial.jp/chapters/following_users?version=5.1#fig-user_has_many_following)を参考にして、id=2のユーザーに対して`user.following`を実行すると、結果はどのようになるでしょうか?

```
>> user = User.find(2)
>> user.following
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com">]
```

このようになるのではないでしょうか。

## 2.2. また、同じユーザーに対して`user.following.map(&:id)`を実行すると、結果はどのようになるでしょうか?

```
>> user = User.find(2)
>> user.following.map(&:id)
=> [1]
```

このようになるのではないでしょうか。

# User/Relationshipの関連付け

別記事で解説します。

* [Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデル - User/Relationshipの関連付け](https://qiita.com/rapidliner00/items/2f68f3cda401c1c5227d)

# 演習 - User/Relationshipの関連付け

## 1. コンソールを開き、[表 14.1](https://railstutorial.jp/chapters/following_users?version=5.1#table-association_methods_relationships)の`create`メソッドを使ってActiveRelationshipを作ってみましょう。データベース上に２人以上のユーザーを用意し、最初のユーザーが２人目のユーザーをフォローしている状態を作ってみてください。

```
>> active_relationship = User.first.active_relationships.create(followed_id: 2)
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
   (0.2ms)  SAVEPOINT active_record_1
  User Load (0.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (24.2ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2020-01-24 10:14:20.701055"], ["updated_at", "2020-01-24 10:14:20.701055"]]
   (0.3ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2020-01-24 10:14:20", updated_at: "2020-01-24 10:14:20">
```

`active_relationships.create`メソッドにより、「id=1のユーザーがid=2のユーザーをフォローしている」という状態を作ったところです。以下のような処理が実行されているのがわかります。

1. `users`テーブルに対してSQLの`SELECT`文を発行し、フォローするユーザーとフォローされるユーザー双方の実在を確認する
2. `relationships`文に対してSQLの`INSERT`文を発行し、実際にリレーションシップを作成する
3. `active_relationships.create`メソッドの戻り値は、作成されたリレーションシップとなる

## 2. 先ほどの演習を終えたら、`active_relationship.followed`の値と`active_relationship.follower`の値を確認し、それぞれの値が正しいことを確認してみましょう。 

```
>> active_relationship.followed
=> #<User id: 2, name: "Dewitt Kub", email: "example-1@railstutorial.org", ...略>

>> active_relationship.follower
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", ...略>
```

先ほど作成したリレーションシップは「id=1のユーザーがid=2のユーザーをフォローする」というものだったので、「`active_relationship.followed`がid=2のユーザー」「`active_relationship.follower`がid=1のユーザー」となるのは、確かに正しいです。

# Relationshipのバリデーション

別記事で解説します。

* [Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデル - Relationshipのバリデーション](https://qiita.com/rapidliner00/items/ac23480d4d54d7c4708e)

# 演習 - Relationshipのバリデーション

## [リスト 14.5](https://railstutorial.jp/chapters/following_users?version=5.1#code-relationship_validations)のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。

> (以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。) 

`app/models/relationship.rb`の内容が以下のように変更された場合、`app/models/relationship.rb`に対するテストの結果はどうなるでしょうか。

```diff:app/models/relationship.rb
  class Relationship < ApplicationRecord
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
-   validates :follower_id, presence: true
-   validates :followed_id, presence: true
  end
```

実際にテストを行っていきます。

```
# rails test test/models/relationship_test.rb
Running via Spring preloader in process 142
Started with run options --seed 9230

  3/3: [===================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.83259s
3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
```

テストは成功しましたね。

# フォローしているユーザー

別記事で解説します。

* [Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデル - フォローしているユーザー - Qiita](https://qiita.com/rapidliner00/items/be2828af432229c09e69)

# 演習 - フォローしているユーザー

## 1.コンソールを開き、[リスト 14.9](https://railstutorial.jp/chapters/following_users?version=5.1#code-utility_method_tests)のコードを順々に実行してみましょう。

```
>> user = User.first
=> #<User id: 1, ...略>

>> other_user = User.second
=> #<User id: 2, ...略>

>> user.following?(other_user)
=> false

>> user.follow(other_user)
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, ...略>]>

>> user.following?(other_user)
=> true

>> user.unfollow(other_user)
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, ...略>

>> user.following?(other_user)
=> false
```

なお、`follow`メソッドの戻り値の型である`ActiveRecord::Associations::CollectionProxy`については、以下のような言及があります。

* [ActiveRecord::Associations::CollectionProxyとは何なのか #13 - Hyurakuのエンジニア日誌](https://hyuraku.hateblo.jp/entry/2018/10/15/230340)

## 2. 先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。 

※以下、SQL文の内容がわかりやすいようにするために、適宜改行や字下げを挿入しています。

### `user.following?(other_user)`

以下のようなSQL文が発行されました。SQL文の前には`User Exists`とついています。

```sql:
SELECT 1 AS one
  FROM "users"
    INNER JOIN "relationships"
      ON "users"."id" = "relationships"."followed_id"
  WHERE
    "relationships"."follower_id" = ?
    AND "users"."id" = ? LIMIT ?
  [
    ["follower_id", 1],
    ["id", 2],
    ["LIMIT", 1]
  ]
```

なお、`User Exists`なSQL文に使われる「`SELECT 1 AS one`」というのは、「レコードそのものは必要としないので捨て、常に`1`という定数を返す[^レコード存在チェックのSQLで何を返すか]」ということを意味するそうです。

[^レコード存在チェックのSQLで何を返すか]: 「レコードの存在をチェックする目的の`SELECT`文で、何を返すのがよいか」というテーマでの問答として、[SQL - EXISTS内のSELECTの列指定｜teratail](https://teratail.com/questions/3954)があります。

### `user.follow(other_user)`

`user.follow(other_user)`というメソッドを実行した際、「`relationships`テーブルにレコードを追加する」という処理に対して発行されるSQL文は以下のようになります。

```sql:
INSERT INTO
  "relationships"
    ("follower_id", "followed_id", "created_at", "updated_at")
    VALUES (?, ?, ?, ?)[
      ["follower_id", 1],
      ["followed_id", 2],
      ["created_at", "2020-01-27 22:43:05.533738"],
      ["updated_at", "2020-01-27 22:43:05.533738"]
    ]
```

`follow`メソッドの戻り値を返すために、以下のようなSQL文も発行されています。

```sql:
SELECT "users".*
  FROM "users"
    INNER JOIN "relationships"
      ON "users"."id" = "relationships"."followed_id"
  WHERE "relationships"."follower_id" = ? LIMIT ?  [
    ["follower_id", 1],
    ["LIMIT", 11]
  ]
```

前述の通り、`follow`メソッドの戻り値は`ActiveRecord::Associations::CollectionProxy`型のオブジェクトとなります。

### `user.unfollow(other_user)`

`user.unfollow(other_user)`というメソッドを実行した際、「`relationships`テーブルからレコードを削除する」という処理に対して発行されるSQL文は以下のようになります。

```sql:
DELETE
  FROM "relationships"
  WHERE "relationships"."id" = ?  [
    ["id", 1]
  ]
```

実際に`DELETE`文が発行する得る前に、以下のSQL文が発行されています。`unfollow`メソッドの戻り値は、以下のSQL文により返ってくる`relationship`型のレコードとなります。その型は`Relationship`型です。

```
SELECT "relationships".*
  FROM "relationships"
  WHERE "relationships"."follower_id" = ?
    AND "relationships"."followed_id" = ?
    LIMIT ?
    [
      ["follower_id", 1],
      ["followed_id", 2],
      ["LIMIT", 1]
    ]
```

# フォロワー

`user.following`メソッドに続いては、`user.followers`メソッドの実装も必要となります。

## 受動的関係の実際のユースケース

受動的関係の実際の実装は、能動的関係の実装と非常に類似しています。というか、使用する交差テーブルは能動的関係と同じものになります。

受動的関係の実際のユースケースは、以下のような図で模式化することができます。

![Passive_Relationship.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/91337c7d-ed2b-47e8-9d3a-1bbdfd583cea.png)

上図では交差テーブルの名前が`passive_relationships`とありますが、実際のアプリケーションでは能動的関係と交差テーブルを共用するので、正式なテーブル名は`relationships`となります。ポイントは以下です。

* 能動的関係と同じく`relationships`テーブルを用いる
* 1つの`followed_id`に対し、`relationships`テーブルを通じて複数の`follower_id`が紐付けられている

## Userモデルにおける受動的関係の実装

関係そのものの名前は`passive_relationships`とします。実際の実装は以下のようになります。

```ruby:
has_many :passive_relationships, class_name: "Relationship",
                                foreign_key: "followed_id",
                                  dependent: :destroy
has_many :followers, through: :passive_relationships, source: :follower
```

現時点のUserモデルに追加するコードは以下のようになります。

```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 :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
    has_many :following, through: :active_relationships,  source: :followed
+   has_many :followers, through: :passive_relationships, source: :follower
    ...略
  end
```

### `source: follower`は、省略可能だがあえて明記している

```ruby:
has_many :followers, through: :passive_relationships, source: :follower
```

上記のコードにおける`:source`キーは、実は省略可能です。「関係性の名前を`:followers`と定義していること」がポイントですね。この場合、Rails[^Active Record]自身が持つ機能により、`followers`の単数形から`follower_id`という名前のカラムが暗黙的に検索されます。

[^Active Record]: より正確には、RailsのActive Recordライブラリです。

それでもあえて`:source`キーを明記しているのは、Railsチュートリアル本文における記載によれば、「`has_many: :following`との類似性を、見た目上強調する意味がある」ということです。「一目見てコードの意味がわかることは重要」というのは、プログラミングの大原則であり、理にかなったものです。

## `followers`に対するテスト

`followers`関係性の動作は、`followers.include?`メソッドによってテストできます。

```diff:
  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)
+     assert mkirisame.followers.include?(rhakurei)
      rhakurei.unfollow(mkirisame)
      assert_not rhakurei.following?(mkirisame)
    end
  end
```

ここまでの実装が問題なくなされていれば、上記のテストは成功するはずです。

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 292
Started with run options --seed 64495

  14/14: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.14936s
14 tests, 26 assertions, 0 failures, 0 errors, 0 skips
```
また、テストスイート全体も成功するはずです。

```
# rails test
Running via Spring preloader in process 305
Started with run options --seed 592

  65/65: [=================================] 100% Time: 00:00:11, Time: 00:00:11

Finished in 11.40050s
65 tests, 338 assertions, 0 failures, 0 errors, 0 skips
```

# 演習 - フォロワー

## 1. コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーを`user`とすると、`user.followers.map(&:id)`の値はどのようになっているでしょうか?

```
>> User.second.id
=> 2
>> User.third.id
=> 3
>> User.forty_two.id
=> 42

>> user = User.first
>> User.second.follow(user)
>> User.third.follow(user)
>> User.forty_two.follow(user)

>> user.followers.map(&:id)
=> [2, 3, 42]
```

## 2. 上の演習が終わったら、`user.followers.count`の実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。

```
>> user.followers.count
=> 3

>> user.followers.count == (user.followers.map(&:id)).count
=> true
```

確かに一致していますね。

## 3. `user.followers.count`を実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、`user.followers.to_a.count`の実行結果と違っている箇所はありますか?

> ヒント: もし`user`に100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

`user.followers.count`を実行した際には、**毎回**以下のようなSQL文が発行されます。

```sql:
SELECT COUNT(*)
  FROM "users"
    INNER JOIN "relationships"
      ON "users"."id" = "relationships"."follower_id"
  WHERE "relationships"."followed_id" = ?
  [
    ["followed_id", 1]
  ]
```

「`user.followers.count`の結果はキャッシュされず、SQL文が毎回発行される」というのは大きなポイントですね。

`user.followers.to_a.count`を実行した際には、**初回に限り**以下のようなSQL文が発行されます。レコードが既に読み込まれている場合、読み込まれたレコードを使うため、SQL文は発行されません。

```sql:
SELECT "users".*
  FROM "users"
    INNER JOIN "relationships"
      ON "users"."id" = "relationships"."follower_id"
  WHERE "relationships"."followed_id" = ?
  [
    ["followed_id", 1]
  ]
```

`user.followers.to_a.count`の場合、`users`テーブルのレコードの全カラムをアプリケーション側に読み込んでいます。しかしながら、結果がキャッシュされるため、SQL文の発行は1回で済みます。

レコード件数が多くなってくると、RDBへの問い合わせによるオーバーヘッドもどんどん大きくなっていきます。レコードの件数のデータが複数回必要になる場合、「アプリケーション側に結果を一時保存するような呼び出し方」を使うことを心がけたいものです。

### 余談 - `user.followers.size`

```
>> user.followers.size
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 3

>> user.followers.size
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 3

>> user.followers.to_a.count
  User Load (0.5ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 3

>> user.followers.to_a.count
=> 3

>> user.followers.size
=> 3

>> user.followers.count
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 3
```

`user.followers.size`の動作は以下のようになります。

* `user`の中身がキャッシュされていない場合、SQL文を発行して`user.followers`の件数を出力する
    * この場合、`user.followers.count`と同じ動作となる
* `user`の中身がキャッシュされている場合、ローカルのキャッシュから`user.followers`の件数を出力する
    * RDBにはアクセスしない
    * この場合、毎回SQL文を発行する`user.followers.count`とは異なる動作となる

`user.followers.size`については、[Railsアプリケーションを遅くする、ActiveRecordの3つの間違い：Count,Where,Present | Scout APM Blog](https://scoutapm.com/blog/three-activerecord-mistakes-ja)の記述を参考にさせていただきました。