18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsでユーザーフォロー機能を実装する(Ajax使うよ)①

Last updated at Posted at 2021-01-17

何をしたか

Railsの課題を実施しています。Userのフォロー機能を実装することになりました。
この機能は前に実装したことはあって、**「確かあのアソシエーションが難しいやつ」**ぐらいには覚えていました。

たまたま、この機能の実装を人に教える機会もありそうだったので、しっかり教えられるように手順を細かく記すことにしました。

ただ、かなりの長文になってしまったので前後編に分けています。この記事ではDB設計〜アソシエーションまでの話をして、続きは下記の記事になります。

Railsでユーザーフォロー機能を実装する(Ajax使うよ)②

なお、実行環境は以下の通りです。

  • Rails 5.2.3
  • Ruby 2.6.0

ゴール

今回、作るものはユーザーのフォロー機能です。ボタンを押すと、ユーザーをフォロー、もう一度ボタンを押すと、フォロー解除できます。

Image from Gyazo

また、この機能のために作成したモデルは下記の通りです。

Image from Gyazo

謎すぎる形態をしていますよね:sweat_smile:
いきなり、上記のモデルに行き着くのは難しいので、ひとつひとつ分解して考えていきます。

なお、以下はあくまでも**「私はこう考えた」**考え方ですので、人によっては他の考え方の方がしっくりくるかもしれません。

実装

分解して考える

まずは、「ユーザー」と「フォロワー」という登場人物を作った方がわかりやすいので、
Userモデルを**(頭の中で)**UserモデルとFollowerモデルに分解します。

Image from Gyazo

この中で、一番簡単そうなUserがたくさんの人をフォローしている状況から考えていきましょう。

Image from Gyazo
この場合、Userモデルに対し

has_many: followings, through: :relationships

というアソシエーションを**(頭の中で)**仮置きします。ただし、Userモデル = Followerモデルであることは忘れないでください。

上記の関係を、マイグレーションファイルで表すとこうなります。

db/migrate/XXXXXXXX_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.2]
  def change
    create_table :relationships do |t|
      t.references :user
      t.references :follower, foreign_key: { to_table: :users }

      t.timestamps

      t.index [:user_id, :follower_id], unique: true
    end
  end
end

followersテーブルは実際には存在しないので、foreign_key: { to_table: :users }で、**「followerを探すときはusersテーブル(のfollower_id)を見てね」**とRailsに伝えています。

この辺りの実装は、↓この記事が大変参考になりました。

マイグレーションにおいて参照先テーブル名を自動で推定できないカラムを外部キーとして指定する方法

また、同じ人を2回フォローできないように

t.index [:user_id, :follow_id], unique: true

user_idカラムとfollower_idのカラムの組み合わせに、重複した値が入らないようにする制約を加えています。

アソシエーションの記載

Usersモデル

この時、Userモデルのアソシエーションの記述は下記のようになります。

models/user.rb
class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follower
end

ポイントは、以下の通りです。

若干複雑になってきましたね...:frowning2:
難しそうなところにはリンクも貼りましたので、ひとつづつ読み解いてみてください。

ところで、Followerモデルなんてないのに、どうやってFollowerモデルを参照するの??という件については、次のRelationshipモデルのアソシエーションをご覧ください。

Relationshipモデル

Relationshipモデルの記載内容は下記の通りです。

models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
end

このように記載することで、Followerモデルの参照がきたらUserモデルを参照するようにRailsに伝えています。

余談: sourceclass_nameの違いについて

ところで、さっきはsourceで別のモデルを参照したのに、今度はなんでclass_nameなの?って思いませんでしたか?(私は思いました)

その理由は、こちらに書いてあって、
Rails: difference between :source => ?? and :class_name => ?? in models

  • source ... has_many :****, through: :****の時に使う
  • class_name ...has_many :***の時に使う

だそうです:smile:知らなかった!!

確認

ここで、一回きちんとアソシエーションが定義できているか確かめることをお勧めします。私は、こんなふうに確かめました。

$ rails c
> user = User.first
> user.relationships
# => []
> user.relationships.create!(follower_id: 2)
# => Relationship のレコードが表示
> user.followings
# => フォローしているUser(user_idが2のユーザー)のレコードが表示

フォロー中のユーザーのレコードが呼び出されたので、id:1のユーザーはid:2のユーザーをフォローできていると言えます。

逆の関係も考える

さて、それでは今度はユーザーがたくさんのフォロワーにフォローされているという、先程どは逆の状況も考えてみたいと思います。

まず、ゴールなのですが、先程の逆の矢印を定義できれば良いです。
具体的に図に書き込んでみます。

Image from Gyazo

followerがたくさんのuserrelationshipsを通じて持っている、こんな図が出来上がります。

でも、Followerモデルは実際には存在せず、Userモデルとイコールでしたね。
そのため、主語をUserに書き換えてみます。

Image from Gyazo

Userrelationshipsを通じて、followersをたくさん持っている。
言葉としてはわかりやすいのですが、今度は、頭の中に作ったFollowモデルが邪魔になってきました。

followersテーブルを置いたために考えるのが難しくなってきているので、FollowモデルとUserモデルを一つにまとめました。

Image from Gyazo

だいぶ、一番初めに提示したER図に近づいてきましたね:grin:

ついでに、先程の赤い矢印も復活させてみました。
すると、今度は**through: :relationshipsの部分で、名前が重複している**のがわかります。

Image from Gyazo

そこで、今度は片方の名前を変えて、userfollowerにフォローされている、と言う意味でpassive_relationshipsと名付けました。

これで、最初に提示したER図と同じになっています:relaxed:

アソシエーションの記載

Userモデル

では、この関係を実際にモデルに表していきます。
まず、Userモデルはこうなります。

models/user.rb
class User < ApplicationRecord
  has_many :relationships, dependent: :destroy
  has_many :followings, through: :relationships, source: :follower

  has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy # 追記1
  has_many :followers, through: :passive_relationships, source: :user # 追記2
end

めっちゃ複雑になってきましたね:joy:ひとつひとつ読みといていくと、まず、追記1の部分は

  • Userはたくさんのpassive_relationshipsを持っています。
  • この時、参照して欲しいクラスはRelationshipです。
  • 外部キーとしてfollower_idを使います。

というのを表しています。
次に、追記2の部分は

  • Userはたくさんのfollowersを、passive_relationshipsを通じて持っています。
  • passive_relationshipsは、直前の定義によりRelationship、クラスを参照するようになっています。

複雑すぎて憤死しそうです:joy:
ワンステップずつ丁寧に書いたつもりなので、丁寧に読んでみてください:sweat_smile:

Relarionshipモデル

最後に、Relationshipモデルに追記した内容を紹介します。

models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :user # 追記
  belongs_to :follower, class_name: 'User'
end

こちらは逆にシンプルすぎて心配になるレベルですが、こちらはこれで完了です:innocent:

確認

ここで、1回確認をしてみます。先程、コンソールでid:1のユーザーがid:2のユーザーをフォローすると言うデータは作りましたので、id:2のユーザーは、id:1のユーザーにフォローされているはずです。

それを、コンソールで試してみます。

$ rails c
> user2 = User.second
> user2.followers
# => id:1のUserのレコードが表示される

user2をフォローしているユーザーが取得できました!!成功です:smile:

to be contenued ...

これからいよいよviewやcontrollerを実装!...となっていくのですが、
ここからは次回に続きます。

Ajax出てこないじゃん!と思った方、ごめんなさい。次回に出てきます。。。

▼この話の続編はこちら
Railsでユーザーフォロー機能を実装する(Ajax使うよ)②

18
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?