💎

Railsでユーザーフォローの機能を作る(テーブル設計〜アソシエーションまで)

2022/02/10に公開約8,500字

はじめに

この記事は初心者向けに、Railsを使って、ユーザーフォローの機能を作る際の考え方を整理したものです。

必要な前提知識

  • Railsのアソシエーションの基礎(has_many、belongs_toなど)
  • ここに書いてあるような、中間テーブルの基本的な内容を理解している(あるいは記事を読んで理解できる)

大まかな流れ

  • テーブルを設計する
  • モデルを作成する
  • マイグレーションファイルを作成する
  • マイグレーションを実行する
  • アソシエーションを書く

という感じで進めていきます。

テーブルを設計する

まず、大前提として、必要なテーブルを考える必要があります。ユーザーフォローの機能なので

  • フォロー数
  • フォローしているユーザーの一覧
  • フォロワー数
  • フォロワーのユーザーの一覧

あたりが計算、表示できると良さそうです。
なので、まずはUserテーブルが必要です。これは簡単ですね。
Userテーブルは

id name
1 A
2 B
3 C
4 D
3 E

のような形とします。フォロー、フォロワーの関係性を考えるとユーザーがユーザーをフォローし、ユーザーはユーザーにフォローされるという感じになりますね。これを見ると、多対多のアソシエーション…なのですが、ちょっと普通の多対多のテーブルと違い、対になるテーブルがUserテーブルで同じものになっています。こういう場合、テーブル設計でどのようにすればいいのでしょうか?

まずは 「User同士のフォローの関係性」を表すテーブルを作成する
ということが必要になります。
今回は、これをFollowRelationshipテーブルとします。
このテーブルには、Userのidを使ってUserのidが1のユーザーが、Userのidが2のユーザーをフォローしている、といったような情報を持つようにします。

ユーザー同士のフォローの情報というのは
フォローをしているユーザーのidと、フォローをしたユーザーのidがあれば表現できるので、例えば以下のようにします。

以下のuser_idはフォローをしているユーザーのid、follow_idはフォローをしたユーザーのidだと思ってください。

id user_id follow_id
1 1 2
2 1 3
3 2 1
4 3 4
5 4 5
6 5 2

少し長くなりますが、文で書くと

  • AはBをフォローしている(=BはAにフォローされている)
  • AはCをフォローしている(=CはAにフォローされている)
  • BはAをフォローしている(=AはBにフォローされている)
  • CはDをフォローしている(=DはCにフォローされている)
  • DはEをフォローしている(=EはDにフォローされている)
  • EはBをフォローしている(=BはEにフォローされている)

という状態を表しています。

実際にデータをいくつか作ってみると、どういう風になるのかより理解が深まると思いますので、そちらも書いてみます。

この表を見ると

  • user_idが1のユーザーは、follow_id(user_id)2,3のユーザーをフォローしている
  • 逆に、follow_id(user_id)が1のユーザーはuser_idが2,3,4のユーザーにフォローされている

ということが読み取れます。

実際のデータを見ると、ユーザー同士の関係性(フォローしているとか、されているとか)を見るためのテーブルがあれば問題ないということがわかるかと思います。

普通、多対多の関係を構成するときには、例えばUserとCourse(授業)のような別々のテーブルがあって、「Userは複数のCourseを選択する」、「CourseにはUserが複数参加する」という状態なので、それをうまく表現するためにUserCourseのような中間テーブルを使います。

ですが、今回は、UserとUserという同じテーブルを使うことになるので、上記のような単純な考え方だと、うまくいかない部分があります。

この状況を回避するために、上記のようなテーブル構成にしたわけです。

モデルを作成する

今回はUserモデルはすでにあるという前提なので、中間テーブルのFollowRelationshipテーブルを作成します。

rails g model FollowRelationship

マイグレーションファイルの編集

上記のコマンドで作成された db/migrate/20220202025251_create_follow_relationships.rb のような名前のファイルを編集していきます。

まず、モデルを作成した際にできているマイグレーションファイルは

db/migrate/20220208000814_create_follow_relationships.rb
class CreateFollowRelationships < ActiveRecord::Migration[6.1]
  def change
    create_table :follow_relationships do |t|
      
      t.timestamps
    end
  end
end

のようになっています。

これにカラムを追加します。詳しくは次に解説しますが、以下のようにファイルを書き換えます。

db/migrate/20220208000814_create_follow_relationships.rb
class CreateFollowRelationships < ActiveRecord::Migration[6.1]
  def change
    create_table :follow_relationships do |t|
      t.references :user, foreign_key: true
      t.references :follow, foreign_key: { to_table: :users }
      
      t.timestamps
    end
  end
end

今回はuser_idとfollow_idを参照すれば良いので、 t.reference を使ってidをカラムに入れられるようにします。今回はUserテーブルと(架空の)Followテーブルを使うので、それぞれ t.references :usert.references :follow と記述します。それぞれのidは存在しない値は入って欲しくないので、外部キー制約をつけます。外部キーの制約は foreign_key: true で設定できるのですが、今回はFollowテーブルという架空のテーブルがあり、その実態はUserテーブルであるという特殊な事情があるので、そうはいきません。(そのまま書くとFolllowテーブルを探しにいってしまう)
なので、t.references :follow の後ろには foreign_key: { to_table: :users } と書いて、参照先のテーブルはUserテーブルだよ!と教えてあげるようにします。

これに加えて、user_idとfollow_idの組み合わせはたった1つである必要があります。user_id=1のユーザーが、follow_id=2のユーザーをフォローしているという状況は1つだけだからですね。
その制約を追加すると、マイグレーションファイルは以下のようになります。

db/migrate/20220208000814_create_follow_relationships.rb
class CreateFollowRelationships < ActiveRecord::Migration[6.1]
  def change
    create_table :follow_relationships do |t|
      t.references :user, foreign_key: true
      t.references :follow, foreign_key: { to_table: :users }
      
      t.timestamps
      
      t.index %i[ user_id follow_id ], unique: true
    end
  end
end

t.index %i[ user_id follow_id ], unique: true でその制約を付与しています。

これで、マイグレーションファイルの編集は完了です。

マイグレーションを実行する

お馴染みのコマンドを叩きます。

rails db:migrate

これで、中間テーブルの設計をアプリケーションに反映することができました。(本来はUserテーブルも対応する必要がありますがここでは割愛しています)

アソシエーションを書く

アソシエーションは、一番わかりやすいところから考えてみましょう。

ユーザーは、フォローしているとかフォローされているとかの情報を複数持っています。
Userテーブルは複数のFollowRelationshipテーブルを持っていることになるので has_many を使って表現します。

app/models/user.rb
class User < ApplicationRecord
  has_many :follow_relationships
end

逆にFollowRelationshipテーブルから見ると、Userテーブルに所属していることになるので、 belngs_to を使って表現できます。

app/models/follow_relationship.rb
class FollowRelationship < ApplicationRecord
  belongs_to :user
end

次に、FollowRelationshipテーブルを中間テーブルとして使うためのコードを追加します。中間テーブルとして使うためにどうするか…というところなんですが、先ほど ユーザーのidと、フォローをしたユーザーのidがあれば表現できる と書きました。

すでにユーザーのidは使えるようになっているので、あとはフォローをしたユーザーのidがあれば良さそうです。つまり、FollowRelationshipテーブルに、follow_idが使えれば良いので、そのための設定を記載します。

app/models/follow_relationship.rb
class FollowRelationship < ApplicationRecord
  belongs_to :user
  belongs_to :follow, class_name: 'User'
end

FollowRelationshipテーブルの中に1行追加しました。

これは、Followテーブルという架空のテーブルを作り、それに対してFollowRelationshipテーブルが所属しているという内容を記述しています。しかしそれだけだと、実在しないテーブルを参照しに行ってしまうので、 class_name のオプションを使って、参照するのはUserテーブルだよ、ということを教えてあげています。

では、Userテーブルの方はどうか。ちょっとここはわかりにくいのですが、FollowRelationshipテーブルからみると、belongs_toが2つ書かれています。しかも、どちらもUserテーブルが対象です(FollowテーブルもUserテーブルなので)

それをそのまま受け取ってUserクラスのコードにすると

app/models/user.rb
class User < ApplicationRecord
  has_many :follow_relationships
  has_many :follow_relationships
end

となってしまい、全く同じ内容を書くことになります。これではダメなので、FollowRelationshipテーブルは、外部キーとしてuser_idとfollow_idをカラムとして持っている ということを利用して、以下のように書いてみます。

app/models/user.rb
class User < ApplicationRecord
  has_many :following_relationships, class_name: 'FollowRelationship', foreign_key: 'user_id'
  has_many :followwer_relationships, class_name: 'FollowRelationship', foreign_key: 'follow_id'
end

外部キーをこのように明示的に書くと、user_idやfollow_idを起点にしてデータにアクセスできるようになります。(つまり、データへの入り口を作るようなイメージ)

そして、分かりやすくするために、中間テーブルの名前も変更しました。class_nameオプションを使って、正しいテーブル名を教えてあげるようにします。

ここまできたら、あと少しです。中間テーブルを使っているので、 has_many :テーブル名, through: :中間テーブル名 の形を使って、テーブル同士が中間テーブルを通じてつながっていることを表現します。

中間テーブルを使った場合、何も考えずに書けば、以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  has_many :following_relationships, class_name: 'FollowRelationship', foreign_key: 'user_id'
  has_many :follower_relationships, class_name: 'FollowRelationship', foreign_key: 'follow_id'
  has_many :follows, through: :follow_relationships
end

UserテーブルがFollowRelationshipテーブルを通じてFollowテーブルにアクセスするという点は、これでも良さそうです。

しかし、今回はFollowテーブルというテーブルは存在しませんし、これではFollowテーブル側から中間テーブルを通じてUserテーブルにアクセスすることができない状況です。

ruby:app/models/user.rb のファイルの中で、それらの問題を解消するにはどうしたら良いのでしょうか。

まず、has_manyを使った箇所を以下のように書き換えます。

app/models/user.rb
class User < ApplicationRecord
  has_many :following_relationships, class_name: 'FollowRelationship', foreign_key: 'user_id'
  has_many :follower_relationships, class_name: 'FollowRelationship', foreign_key: 'follow_id'
  has_many :followings, through: :following_relationships
end

followings という名前の架空のテーブルを作り出し、中間テーブルはすでに定義した following_relationships という名前のFollowRelationshipテーブルを通すようにします。

これで良さそうなのですが、この書き方だとFollowingという存在しないテーブルを参照しにいってしまいます。

そのため、ここでsourceオプションを使って、データを取得しにいくテーブルを教えてあげます

app/models/user.rb
class User < ApplicationRecord
  has_many :following_relationships, class_name: 'FollowRelationship', foreign_key: 'user_id'
  has_many :follower_relationships, class_name: 'FollowRelationship', foreign_key: 'follow_id'
  has_many :followings, through: :following_relationships, source: :follow
end

これで、UserテーブルからFollowRelationshipテーブルを通ってFollowテーブルに到達することはでき、user.followings のような形でフォローしたユーザー一覧にアクセスできるようになりました!

そして最後にその逆をしてあげれば、今回やりたいことが達成できます。なので、素直にやってみましょう。

Followerという架空のテーブルを作り、同じように has_many :テーブル名, through: :中間テーブル名 の形を使って書きます。

app/models/user.rb
class User < ApplicationRecord
  has_many :following_relationships, class_name: 'FollowRelationship', foreign_key: 'user_id'
  has_many :follower_relationships, class_name: 'FollowRelationship', foreign_key: 'follow_id'
  has_many :followings, through: :following_relationships, source: :follow
  has_many :followers, through: :follower_relationships, source: :user
end

勘の良い方は気がつかれたかもしれませんが、foreign_keyが入り口となり、sourceで指定したテーブル名が出口となるようなイメージで、中間テーブルを通してデータを取得できるようになっています

ユーザー同士のフォローが分かりにくいのは、単純な中間テーブルの多対多の関係性とは違い、1つのUserというテーブルと中間テーブルのみでUserテーブル同士の多対多の関係性を表現しなければならない、という点にあると思います。

自分の理解を深めるために今回は記事にまとめてみました。

※動作確認などした上で、もし間違っている箇所があったらできるだけ早めに直していきます🙇‍♂️

Discussion

ログインするとコメントできます