多対多の関係について
本記事の内容
フィヨルドブートキャンプでアプリにフォロー機能を実装するという課題がありました。
課題の趣旨として、フォロー機能の核となる多対多の関連付けを正しく実装するという目的があります。
本記事では課題を実装する上で理解するのが難しかったモデルの設計について備忘録としてまとめます。
モデルの関連付けについて
フォロー機能の厄介な点として、フォローするユーザーとフォローされるユーザーが存在することが挙げられます。
このような場合、その関係は1対Nではなく、多対多となります。
1対多の関連付けについては、お馴染みですね。下記のようにhas_manyとbelongs_toを用いて親子関係を表現します。
class Author < ApplicationRecord has_many :books, dependent: :destroy end class Book < ApplicationRecord belongs_to :author end
では、多対多の関係はどのように関連づけるのでしょうか。
まず、フォロー機能実装にあたりFriendshipというモデルを考えます。
これが中間テーブルの役割に該当するのですが、多対多をいきなり考えるとよくわからなくなる(少なくとも私はよくわからなくなりました)ので、まず1対多の関係を考慮します。
1人のユーザーが複数をフォローする場合、つまりフォローする側から考える能動的関係(active_relationship)を考慮します。
能動的関係(active_relationship)
id:1のユーザーが複数をフォローする図式が以下のようになりますね。
| id | 氏名 | |
|---|---|---|
| 1 | 山田太郎 | yamada.tarou@gmail.com |
| follower_id | followed_id |
|---|---|
| 1 | 2 |
| 1 | 10 |
| 8 | 1 |
| 1 | 7 |
| 4 | 1 |
| id | 氏名 | |
|---|---|---|
| 2 | ... | ... |
| id | 氏名 | |
|---|---|---|
| 10 | ... | ... |
| id | 氏名 | |
|---|---|---|
| 7 | ... | ... |
active_relationshipをthrough(通して)、フォローされる側の情報(User.following)を所有する(has_many)というイメージです。
受動的関係(passive_friendship)
次は、フォローされる側(id:1)から見た受動的関係(passive_friendship)です。こちらも1対Nで考えましょう。
これ、先程のFriendshipモデルをひっくり返しただけですよね。
| id | 氏名 | |
|---|---|---|
| 1 | 山田太郎 | yamada.tarou@gmail.com |
| followed_id | follower_id |
|---|---|
| 2 | 1 |
| 10 | 1 |
| 1 | 8 |
| 7 | 1 |
| 1 | 4 |
| id | 氏名 | |
|---|---|---|
| 8 | ... | ... |
| id | 氏名 | |
|---|---|---|
| 4 | ... | ... |
こちらも、passive_relationshipを通して(through)、フォローする側の情報(User.followers)を所有(has_many)するイメージです。
Friendshipという1種類のモデルを、2種類の視点から考慮するのですね。
Friendshipモデル
最後に、Friendshipモデルの関連付けは以下のようになります。
class Friendship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" end
user.rbで定義した外部キーfollowerとfollowedをbelongs_toに記述しています。
問題ないですね。では、実装してみましょう!
実装
まず、User モデルはhas_manyで Friendship モデルを所有しています。
カラムにはどのUserに所有されるかを表す外部キーを持たなくてはなりません。
フォロー機能の場合、フォローする側とフォローされる側という区別が必要ですので、異なる2種類の外部キーが必須となります。
外部キーはフォローする側のfollower_idとフォローされる側のfollowed_idの2種類となります。

いずれも、本来のクラス名はFriendshipなのでclass_name: : "Friendship"というオプションを記載します。
また、ユーザーが削除されたときに自動的にフレンドシップも削除されるようにするため、dependent: :destroy をつけます。
次に、先程のテーブルで確認したようにFriendshipを介してユーザーを取得する処理を考えましょう。

フォローする側のユーザーは、所有するactive_friendshipsを通じてフォローされる側のユーザーfollowedを所有します。
逆に、フォローされる側のUserは、所有するpassive_friendshipsを介してフォローする側のユーザーfollowerを所有します。
sourceはモデルの参照元を明示します。
デフォルトのhas_many throughという関連付けにおいては、Railsはモデルのクラス名 (単数形) に対応する外部キーを探しますが、
今回はsource名に参照先を明示しているお陰で、followed_idやfollower_idから該当ユーザーを取得できます。
source: :followed #フォローされる側 source: :follower #フォローする側
中間テーブルを通して、フォローされる側のユーザーを集めることをfollowing、フォローする側のユーザーを集めることをfollowersと定義しているだけですね。
このhas_many throughの関連付けのお陰で、User.followingやUser.followersでユーザー情報を配列のように取得できます。
# app/models/user.rb def follow(other_user) following << other_user end
上記のメソッドでは、<<演算子を用いてユーザー情報をfollowingnの配列の最後に追記しています。簡単にフォロー機能が実装できますね。
最後に
こちらで記事は終了です。 参考文献: