ユーザーフォローの関連付けを自分の言葉で説明してみる

Rails Tutorial 14章でお馴染みのユーザーフォロー機能。 ユーザー同士の多対多と関係するコードについて、自分の言葉で説明してみたいと思います。 尚、実装例や全ての解説とコードは以下の記事にまとまっていますので、とりあえず動かしたいという方はそちらを読んでください。

qiita.com

ユーザーフォロー概観

  • 1ユーザーはたくさんのユーザーをフォローでき、たくさんのユーザーによってフォローされる
  • このような関係のことを自己結合多対多と呼ぶ
  • 多対多なので、中間テーブル(Relationships)が必要

ゴール

irb(main):002:0> user.followings
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">, #<User id: 4, email: "testuser-4@example.com", created_at: "2020-08-11 00:05:16", updated_at: "2020-08-11 00:05:16", address: nil, postal_code: nil, bio: nil, name: "testuser-4", uid: "4a93e555-40ec-4308-a3d5-c2a59ae03c2e", provider: "">]>

irb(main):004:0> user.followers
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "testuser-2@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-2", uid: "a927f181-595e-4e74-9c29-11367612cb85", provider: "">, #<User id: 3, email: "testuser-3@example.com", created_at: "2020-08-11 00:05:15", updated_at: "2020-08-11 00:05:15", address: nil, postal_code: nil, bio: nil, name: "testuser-3", uid: "d36fa9e5-c0ba-41b0-a386-1122be7e8bde", provider: "">]>

自分がフォローしているユーザー、フォローされているユーザーを取得できるようにします。

ユーザーモデルの切り分け

Railsのコードを書く前に、DBレベルではどのようなデータが保存されているか確認します。(手書きのメモが汚いです すんません..)

f:id:mashoo1101:20200811163710p:plain とてもシンプルですね。 ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている状態を表しています。 DBレベルではデータを保存するだけなのでこの実装で問題ないのですが、Rails側でデータを取り出したいとなった時にひとつ困ることがあります。 「ユーザーはユーザーをたくさん持っていて、ユーザーはユーザーをたくさん持っている」これだけだと、ユーザー間の関係性をうまく表現できません。フォロワーとフォローされる側を明確にする必要があります。 よって以下のコードを追記します。

class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end

この記述により、Railsからは以下のようにデータを扱えるようになりました。

f:id:mashoo1101:20200811163954p:plain

「フォロワーはたくさんのフォローしているユーザーを持っている」&「フォローしているユーザーはたくさんのフォローされているユーザーを持っている」という状態を表現できました。 この記述により最後に実装する「ユーザーの取得」で、適切なユーザーを取得できるようになります。 (あくまでRailsから操作できる仮想的なモデル名であり、DBに保存される情報はコード追記前と変わりません)

Relationshipから適切なデータを取得する

最終目標である「自分がフォローしているユーザー、フォローされているユーザーを取得」するには、中間テーブルであるRelationshipsを使えば書けそうです。 現在の中間テーブルの中身は以下のようになっています。

f:id:mashoo1101:20200811163612p:plain

この中から自分をフォローしているユーザーとの関係(relationship)を表す列を取得するにはどうすれば良いでしょうか? 答えは 「follower_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 逆に自分がフォローしているユーザーとの関係(relationship)を表す列を取得するには、「following_idの値が自分のidの値と同じ列」を取得するよう指定する必要があります。 UserをFollowerとFollowingに分けたように上記二つに名前を付けてあげましょう。 自分をフォローしているユーザーとの関係を「passive_relationships」、自分がフォローしているユーザーとの関係を「active_relationships」とします。 コードは以下のようになります。

class User < ApplicationRecord
   has_many :active_relationships, class_name: 'Relationship', foreign_key: :following_id, dependent: :destroy
   has_many :passive_relationships, class_name: 'Relationship', foreign_key: :follower_id, dependent: :destroy
end

「follower_idの値が自分のidの値と同じ列」を取得する指定はforeign_keyオプションで行います。 dependent: :destroyはユーザーが削除された時、relationshipも削除するという意味になります。

ユーザーの取得

active_relationshipspassive_relationshipsを通してフォロー中/フォロワーのユーザーを取得します。 followingsを例に説明します。自分がフォローしているユーザーとの関係性を表すactive_relationshipsから、自分にフォローされているユーザーを取得します。 コードは以下のようになります

class User < ApplicationRecord
  has_many :followings, through: :active_relationships, source: :follower
  has_many :followers, through: :passive_relationships, source: :following
end

「ユーザーモデルの切り分け」で記述した以下のコードによってどちらのユーザーを取ってくるかsourceを指定できるのです!!

class Relationship < ApplicationRecord
  belongs_to :following, class_name: 'User'
  belongs_to :follower, class_name: 'User'
end

 まとめ

既にユーザーフォローに関する素晴らしい記事は存在しますが、自分の言葉でまとめてみると理解度も上がるなと感じました。 疑問点、間違いの指摘等ありましたらコメントお願いします。