Twitterみたいなユーザ同士をフォローする仕組みを実装するにあたり、ActiveRecordでN:Nの自己結合を実装するのにちょっとハマったので書いておきます。
前提
- usersテーブルがある
- User同士のフォロー関係を登録するfollowsテーブルがある
具体的なmigrationは以下。
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false
t.string :email
# :
# :
t.timestamps
end
end
end
class CreateFollows < ActiveRecord::Migration
def change
create_table :follows do |t|
t.integer :from_user_id, null: false
t.integer :to_user_id, null: false
t.timestamps
t.index :from_user_id
t.index :to_user_id
t.index [:from_user_id, :to_user_id], unique: true
end
end
end
やりたいこと
下記のようなインターフェイスで実装したいとします。
User.find(1).following # フォローしているUserの配列が返る
User.find(1).followed # フォローされているUserの配列が返る
ハマりポイント
- リレーションがN:N
- Railsのデフォルトでは
user_id
を結合に使うが、今回は同じテーブルを結合するので使えない
実装
まずUserからFollow、FollowからUserに1:Nのリレーションをはります。
has_many :follows, foreign_key: :from_user_id
belongs_to :from_user, class_name: User, foreign_key: :from_user_id
こうすると、 User.find(1).follows
でFollowが、 Follow.find(1).from_user
でフォロー元のUserというリレーションができます。次に、これら1:Nのリレーションを結合する設定をUserに追加します。
has_many :follows, foreign_key: :from_user_id
has_many :following, through: :follows, source: :to_user # ← 追加
これで、 User.find(1).following
でフォロー先のUserの配列が取得できるようになりました。followed側も同様に実装できます。
最終形
最終的に下記のようになりました。条件の異なる2つの has_many :follows
が必要になるのでリネームしました。
has_many :follows_from, class_name: Follow, foreign_key: :from_user_id, dependent: :destroy
has_many :follows_to, class_name: Follow, foreign_key: :to_user_id, dependent: :destroy
has_many :following, through: :follows_from, source: :to_user
has_many :followed, through: :follows_to, source: :from_user
belongs_to :from_user, class_name: User, foreign_key: :from_user_id
belongs_to :to_user, class_name: User, foreign_key: :to_user_id
おまけ
下記のようなインターフェイスを追加したくなりました。
user.following?(User.find(1))
user.followed_by?(User.find(1))
最初は下記のように any?
にブロックを渡して評価していました。
def following?(user)
self.following.any? { |u| u.id == user.id }
end
しかしこれでは普通にSELECT文が走って全件インスタンス化しているようだったので下記のようにしてみたところ、 SELECT COUNT(*)
が走るようになったのでこちらのほうがパフォーマンスがよいでしょう。
# 指定ユーザをフォローしているかどうかを返す
def following?(user)
self.following.where(id: user.id).any?
end
# 指定ユーザにフォローされているかどうかを返す
def followed_by?(user)
self.followed.where(id: user.id).any?
end