Railsチュートリアル 第14章 ユーザーをフォローする - Relationshipモデルにおけるデータモデルの問題 (および解決策)

# 「フォロー」という関係の定義

今回開発を行うサンプルアプリケーションでは、ユーザーとユーザーの関係性として、「フォロー(follow)」という関係を定義します。フォロー関係のより具体的な定義は以下の通りです。

* 1人のユーザーは複数のユーザーをフォローする
* 1人のユーザーは複数のユーザーによってフォローされる
* フォロー関係は左右非対称である
    * 「AがBをフォローする」と「BがAをフォローする」は異なるものである
    * この点は、例えばFacebook等における「友好(friendship)」とは異なる関係である

# テーブル名の定義

Railsチュートリアル本文では、フォロー関係のテーブル名の定義として、以下のようなルールを採用することとしています。

* あるユーザー**を**フォローしているユーザーの集合は`followers`とする
* あるユーザー**が**フォローしているユーザーの集合は`following`とする

「AはBのフォロワーである」を英語にすると「A is a follower of B」となります。一方、「BはAによってフォローされている」を英語にすると「B is followed by A」となります。

followerの複数形はfollowersであり、`user.followers`というテーブルは「`user`をフォローしているユーザーの集合」として自然な名前です。しかし、followedという語を複数形にはできません。「`user`がフォローしているユーザーの集合」というテーブルの名前はどうすればいいのでしょうか。`followeds`ですか？いや、それはさすがにまずいでしょう。英語の文法から逸脱しますし、見苦しいですし…

というわけで、「あるユーザーがフォローしているユーザーの集合」というテーブルは、Twitterに倣って`following`という呼称を採用することとします。`user.following`という感じですね。

# 「ユーザーが多数のフォロワーを持っている」という方向性はすぐに行き詰まる

「ユーザーが多数のフォロワーを持っている」という関係性を素朴にモデリングすると、以下のようなモデルになります。

![Naive followship.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/b2ba81ea-5b0b-687a-a289-2c1839cbb3bf.png)

このモデルは何が問題なのでしょう。Railsチュートリアル本文では、「非常に無駄が多いことが問題である」としています。より具体的には、以下のような問題があります。

* `following`テーブル側にも、`name`や`email`といった属性が存在する
    * これらはいずれも、`users`テーブルに既に存在する属性である
* このままで行くと、`followers`をモデリングする際にも、同じくらい無駄が多いテーブルが定義されてしまうのが避けられない
    * 「ユーザーは多数のユーザーにフォローされている」という関係のモデリング

このような無駄の多い実装の存在は、運用側から見れば悪夢といえます。ユーザー名やメールアドレスを変更するたびに、`users`テーブルの当該属性のみならず、`following`テーブルや`followers`テーブルの当該属性を更新しなければならない…考えたくない実装ですね。トリガー等を使わなければならなそうです。リレーションシップ外の結合が発生しそうです。

## ユーザーに`following`や`followers`といったカラムをもたせるわけにもいかない

「ユーザーに`following`や`followers`というカラムをもたせ、フォローしている・フォローされているユーザーのIDをカンマ区切りで入れていく」という実装は、「Jaywalking」と呼ばれ、RDBのテーブル設計として著名なアンチパターンです。

このような実装がもたらす弊害については、例えば以下のような記事にて言及されています。

* [1章 Jaywalking(信号無視) - Qiita](https://qiita.com/iwata@github/items/f33490d34dd1400b166e)

# どんな実装が適切なのか

適切な実装を実現するために、「`following`の動作を、RESTアーキテクチャを使ってどのように実装するか」ということを考えてみます。

RESTアーキテクチャというのは、「リソースの生成や削除に対して何らかのアクションを割り当てる」というのが基本方針でしたね。「ユーザーをフォローする」という操作では、「何らかのリソースが生成される」という動作が発生するのが自然です。また、「ユーザーのフォローを解除する」という操作では、「何らかのリソースが削除される」という動作が発生するのが自然です。

ユーザーのフォロー状態の変化によって生成ないしは削除されるリソース。それは、「任意の2人のユーザー間における関係（リレーションシップ）」とするのが妥当でしょう。すなわち、以下のような関係性ですね。

* 1人のユーザーは、多くの関係を持つことができる
* リレーションシップを通じて、1人のユーザーは多くの`following`（または`followers`）という関係を持つことができる

# 多対多の関係性

ユーザー同士の`following`および`followers`という関係は、より詳しく説明すると以下のようになります。

* 1人のユーザーは、0または1人以上のユーザーをフォローすることができる
* 1人のユーザーは、0または1人以上のユーザーからフォローされることができる

![Followship.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/211f7bf8-6457-22de-e2af-794c4e8544f3.png)

このような関係は「多対多」といいます。Railsチュートリアルの第13章までには登場しなかった関係ですね。多対多の関係は、そのままではRDB上に表現することはできません。このような関係をRDB上で表現する場合、交差テーブル[^交差テーブルの別名]を使って「複数の1対多の関係」に分解する必要があります。

[^交差テーブルの別名]: 中間テーブルや関連テーブルなどとも呼ばれます。

# 「フォロー」という関係は左右非対称である

Twitterのようなフォロー関係においては、例えば「CalvinがHobbesをフォローしていても、HobbesはCalvinをフォローしていない」という関係が発生する場合があります。このような関係を「左右非対称」と呼ぶこととします。

左右非対称な関係をアプリケーションで実装するためには、「AはBをフォローしている」という関係と、「AはBにフォローされている」という関係は明確に区別する必要があります。それぞれに対応する実装が必要になるのですね。

Railsチュートリアル本文では、それぞれの関係性を指す語を以下のように定義しています。

* 「AはBをフォローしている」というような関係…「能動的関係(Active Relationship)」
* 「AはBにフォローされている」というような関係…「受動的関係(Passive Relationship)」

# 「能動的関係」をどう実装していくか

受動的関係の実装は後にして、まずは能動的関係の実装を行っていきましょう。

前述「『ユーザーが多数のフォロワーを持っている』という関係性の素朴なモデリング」において、以下の事実は重要です。

* フォローしているユーザーは、`followed_id`があれば識別することができる

一方で、フォロー関係の表現にあたっては、ユーザーid以外のユーザー情報は必要ありません。なので、「フォロー関係を表現するためのテーブル」に必要な列は、`follower_id`と`followed_id`のみとなります。

これを踏まえて、「1人のユーザーが0人以上の複数のユーザーをフォローし、1人のユーザーが0人以上の複数のユーザーにフォローされる」という関係をER図にすると以下のようになります。

![Followship with Cross Table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/df4a2b70-686d-f4ed-9c41-72474b992de9.png)

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

![Active_Relationship.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/72944b86-f392-1115-6069-4b64df1e4d46.png)

上記模式図には、Railsで用いるキーワードも含まれています。

`through`は初めて見るキーワードですね。「Rails上で、多対多のリレーションシップを交差テーブルを用いて表現する」という場合に用いるキーワードです。より詳細な意味については後で説明します。[Railsガイドにおける、`has_many :through`関連付けについての説明](https://railsguides.jp/association_basics.html#has-many-through%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91)も参考になるでしょう。

# ここまでの内容を踏まえて、新たに生成するテーブル`relationships`の全体像

能動的関係も受動的関係も、最終的には同じ構造のテーブルを使うことになります。したがって、テーブルの名前は「`relationships`」とするのが自然です。Railsの慣習に従い、モデル名はRelationshipとしましょう。`relationships`テーブルの全体像は以下のようになります。

![relationships.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/8c892c4b-2756-401e-9c53-1819ae391a62.png)

## Relationshipモデルに対応するマイグレーションを生成する

上記Relationshipモデルに対応するマイグレーションを生成するコマンドは以下のようになります。

```
# rails generate model Relationship follower_id:integer followed_id:integer

```

上記のコマンドの実行結果は以下のようになるはずです。

```
# rails generate model Relationship follower_id:integer followed_id:integer
      invoke  active_record
      create    db/migrate/[timestamp]_create_relationships.rb
      create    app/models/relationship.rb
      invoke    test_unit
      create      test/models/relationship_test.rb
      create      test/fixtures/relationships.yml
```

## `relationships`テーブルに必要なインデックスを追加する

`relationships`テーブルに対しては、今後`follower_id`および`followed_id`で頻繁に検索を実行することになります。これらの情報を検索するのに、いちいちテーブル全体を検索するのでは、「将来テーブル規模が大きくなった場合のパフォーマンス」という点で話になりません。

また、「`follower_id`の値と`followed_id`の値の組は一意である」という制約も必要です。「あるユーザーが別の同一ユーザーを多重にフォローする」というユースケースは想定しない仕様であるためです。

```diff:db/migrate/[timestamp]_create_relationships.rb
  class CreateRelationships < ActiveRecord::Migration[5.1]
    def change
      create_table :relationships do |t|
        t.integer :follower_id
        t.integer :followed_id

        t.timestamps
      end
+       add_index :relationships, :follower_id
+       add_index :relationships, :followed_id
+       add_index :relationships, [:follower_id, :followed_id], unique: true
    end
  end
```

### 複合キーインデックス

RDB側で「`follower_id`の値と`followed_id`の値の組は一意である」という制約を実装するためには、「複合キーインデックス」というテクニックを使います。Rails[^複合キーインデックスを定義する箇所]で複合キーインデックスを定義するためには、`add_index`の第2引数に「複合キーインデックスを構成する全てのカラムの組」を配列で与えます。「一意である」という制約は、オプションハッシュの`unique:true`により与えられます。

```ruby:
add_index :relationships, [:follower_id, :followed_id], unique: true
```

[^複合キーインデックスを定義する箇所]: より正確には、「Active Recordの、当該複合キーインデックスを定義したいテーブルの生成に対するマイグレーション内」となります。

## 実際に`relationships`テーブルを生成する

ここまでの実装が完了すれば、実際に`relationships`テーブルを生成することができます。いつもの`rails db:migrate`コマンドですね。

記述したマイグレーションの内容に問題がなければ、以下のようなログを出力してマイグレーションが完了します。

```
# rails db:migrate
== [timestamp] CreateRelationships: migrating ==============================
-- create_table(:relationships)
   -> 0.0143s
-- add_index(:relationships, :follower_id)
   -> 0.0025s
-- add_index(:relationships, :followed_id)
   -> 0.0053s
-- add_index(:relationships, [:follower_id, :followed_id], {:unique=>true})
   -> 0.0053s
== [timestamp] CreateRelationships: migrated (0.0360s) =====================
```

### 実は、最初マイグレーションの記述でミスをしてしまった

私自身は、このセクションに取り掛かったとき、最初マイグレーションの反映に失敗しました。理由は、以下のようなコードの記述間違いをしたためです。

```diff:db/migrate/[timestamp]_create_relationships.rb（NG）
  class CreateRelationships < ActiveRecord::Migration[5.1]
    def change
      create_table :relationships do |t|
        t.integer :follower_id
        t.integer :followed_id

        t.timestamps
+     end  # <- endの位置はこっちが正しい
        add_index :relationships, :follower_id
        add_index :relationships, :followed_id
        add_index :relationships, [:follower_id, :followed_id], unique: true
-     end  # <- このendは間違い
    end
  end
```

存在しないテーブルに対してインデックスは生成できませんよね。はい。