8
2

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 1 year has passed since last update.

rails 詳しいフォロー機能の仕組み

Last updated at Posted at 2022-03-25

初めに

今回この記事では、フォロー機能について詳しく解説していきたいと思います。
他の人の記事を読んでも仮想モデルってなんだよ、、、
さっぱりわからんわと感じた人にぜひ読んでいただきたいです。
フォローの記述の設定はこの記事で行いますが、フォロー一覧ページの作成やフォローボタンの作成は行わないので実装だけしたい人は下記で載せさせていただいておりますサイトを参照していただけますと幸いです。

前提の状態

  • テーブル構成(schemaファイル)
    image.png

  • 備考

    • userテーブルは、deviseを使用。
    • 基本的にconsoleからデータを入れてみています。
  • 参考になるフォローの記事
    こちらの記事では、解説から実装までされているので実装をしたい方はこちらを参考に作成していただくとわかりやすいかと思います。
    今回のこの記事では、上記で載せさせていただきました記事を実際に実装して自分の理解が追いつかないところがあったので自分なりにアウトプットした内容になります。

  • ダミーデータの作成(seedファイル)

## db/seeds

email_array = ["a@a", "b@b", "c@c", "d@d"]

email_array.each do |e|
  User.create(
    email: e,
    password: "aaaaaa"
  )
end

user_array = [1,2,3,4]

user_array.each do |follower|
  user_array.each do |followed|
    Relationship.create(
      follower_id: follower,
      followed_id: followed
    )
  end
end

ダミーデータとして、Userとfollowのデータを作成しています。
コピーしてターミナルにて$rails db:seedをする

フォロー機能のテーブルの構成を考えてみる

まず初めに、フォロー機能を作成する上でどのようなテーブルの関係を作成すれば良いでしょうか?
フォロー機能とはUserがUserをフォローする機能になるので、初めはこのような関係を連想されると思います。
image.png
考え方としては正しいのですが、このままだとUserテーブルに対してUserテーブルが紐づいており、テーブルの関係はN対Nというだいぶカオスなテーブルの関係になってしまいます。テーブルの構成を考える上での鉄則として、N対Nの関係のテーブルは存在をしてはいけないという鉄則がありましたね。こちらのサイトにテーブルがN対Nで関係を結べないことが明記されておりますので、知らなかった方は見てから進んでください。
N対Nの関係を持つテーブルがある場合、中間テーブルというものを作成して関係を結ぶのでしたね。
なので、実際にUserのフォロー機能の中間テーブルとしてRelationshipというフォローの関係を記録しているテーブルを作成してみると、、
image.png
綺麗にUserテーブルのフォローの関係が1対Nになり、N対Nの関係を避けることができましたね!!
ここまで来れば、フォロー機能のテーブルの理解は完了です。

フォロー機能のアソシエーションを考えみよう(一番むずかしい)

テーブルの構成が終わったところで、ここからはテーブルのアソシエーションについて考えていきましょう。
ここからはおそらく大半の人が理解に苦しむであろう仮想テーブルも登場しますので、頑張ってついてきてください。

まずUserのテーブルには、どのようなリレーションが必要なのかを考えてみましょう。
image.png
この関係からわかるように、Userのテーブルはrelationshipテーブルに対して二つのリレーションが必要になりますね。
具体的には、、
image.png
フォロー側のリレーションとフォローされる側のリレーションの二つが必要だということがわかります。
この状態だと少しイメージがしずらいかもしれないので、Userとrelationshipのテーブルの関係を戻してみてみると、、
image.png
アソシエーションを記載するUser.rbのファイルは一つしかないので、フォロー側のリレーションもフォローされる側のリレーションも両方同じファイルに記載する必要があることがわかりますね。

そしたら、二つのアソシエーションを結ぶ必要があるので、下記のように書いてみると、、

## user.rb

class user < ApplicationRecord
  has_many :relationships //フォロー側のリレーション
  has_may :relationships //フォローされる側のリレーション
end

ここでふと疑問に思うのが、Userテーブルとrelashionshipテーブルの二つしかないのにどうやってフォロー側とフォローされる側でリレーションを分けて結ぶのだろうという疑問が出てくると思います。
上記のリレーションの結び方では、フォロー側とフォローされる側のリレーションを判別することは残念ながらできません。

そこで、登場するのが仮想モデルです。大体の人がここでフォロー機能の理解を諦めるところかなと思います(僕も初めて実装した時はそうでした笑)
今回どのように仮想テーブルを利用するのかというと、relationshipの中間テーブルの中にフォローを管理するfollowとフォローされる側の管理をするfollowedの二つの仮想テーブルを作成します。
image.png
relationshipの中にこの二つを作成して、Userのテーブルとそれぞれの仮想テーブルとでリレーションを結ぶことによってフォロー側とフォローされる側のアソシエーションを区別するようにします。

実際にどのように仮想テーブルを作成するのかというと、

## relationship.rb (中間テーブルのモデルファイル)

class Relationship < ApplicationRecord
  # follower_id : フォローしたユーザー
  # followed_id : フォローされたユーザー
	
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  
end

belongs_to :follower, class_name: "User"

仮想テーブルを作成する際に使用するオプションが、class_nameになります。
この記述であれば、実際はUserテーブルなんだけど、:followerという仮想テーブルとして扱ってねという設定をしています。

belongs_to :followed, class_name: "User"

こちらの記述も同様に、実際にはUserテーブルのことなんだけど、ここでは:followedという仮想テーブルとして扱ってねという形で仮想テーブルを制作します。
image.png
これで、relationshipの中間テーブルに二つの仮想テーブルが作成されました。

仮想テーブルの作成も終わったところで、実際にUserとリレーションを結んでいきましょう。

## userモデル

class User < ApplicationRecord

  // deviseの記述は紛らわしいため省略

 has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy 
 has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy 

end

has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy

この記述では、先ほど作成したフォローを管理する仮想テーブルの:followerに対して一人のユーザーは多くの人をフォローすることができるのでhas_manyでリレーションを結んでいます。その際に、:followerはRelationshipテーブルの中に作成した仮想テーブルなので、class_nameのオプションでRelationshipテーブルの中にあるものだよと道筋を教えてあげます。そして、このforeign_keyでは、follower_idのカラムの中に入っている値とユーザーのIDが一致しているものを取得するように設定をしている記述になります。そして、最後の記述はおまけですが、Userが退会などをしてUserの情報が消えてしまった場合、フォローの情報も削除をするという設定をしております。

has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy

こちらの記述は:followerの反対で、フォローされている情報を管理するためのアソシエーションを結んでいます。class_nameなどのオプションも全く同じ設定になります。

これで、Userのフォロー機能のアソシエーションの設定は完了になります。

フォローしている人やフォローされている人を一気に取得するために

実は、このままだとフォローをしている人の一覧を取得するにはかなり大変な状態になっております。
image.png
上記の画像のようにconsoleから@user.followerとするとRelationshipモデルのレコードが一覧で返されるようになっています。ここから、フォローしている人のUser情報の一覧を返すには、@user.followerで取得した情報はArray型(配列)で帰ってきているので配列のデータを一つ一つeachやmapを使って分解し、分解した情報の中からfollowed_idを取り出し、取り出したfollowed_idを元にUserテーブルに検索をかけ、検索結果を配列の中に格納するというとても非効率な書き方をしなければなりません。

@user = current_user
// @following_userの中にフォローしている人一覧のUser情報が入る
@following_user = Array.new 

@user.follower.each do |follow|
  @following_user.push(User.find(follow.followed_id))
end
// フォローしている人の一覧を出力する
print(@following_user)

実際にfollowで取り出そうとするとこのように書かないといけないですね〜〜、、
ものすごく見ずらいです、、、(僕のコードが汚いのもありますが笑)
いちいちこんなコード書いてたら、日が暮れちゃうよ、、、@user.following_userで一気に取得するようにしたいと考えるわけです。

そしたらどのようにアソシエーションを結べば一気に取得することができるか考えてみましょう。
現在の状態としては、このようになっています。
image.png
一番上に配置されているUserから直接followedというフォローしている人の一覧を取るにはどのようにアソシエーションを結べば良いでしょうか?
正解は、、
image.png
このように直接Userのモデルからfollowedに対してアソシエーションを結んでしまえば取得することができます。
ではどのようにコードで、アソシエーションを記述するのでしょうか?
今回使用するのは、throughというオプションとsourceというオプションを使用します。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy 
  has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy 
  
  //この下の二行を追加↓
  has_many :following_user, through: :follower, source: :followed 
  has_many :follower_user, through: :followed, source: :follower 
end

今回ここで使用している、throughとsourceについてはこちらを参照ください。
ざっくりいうと、throughとsourceは親モデルから孫要素を取得するために使用します。
親と孫とはどういうことかというと、今回の場合で言えば、User(親要素)はfollowerという(子要素)と関係を持っています。そして、follower(子要素)はfollowed(孫要素)と関係を持っています。つまり、User(親要素)はfollower(子要素)を通じて、followed(孫要素)と関係を持っているということになります。

User(親要素)はfollower(子要素)を通じて、followed(孫要素)と関係を持っている

この部分をコードにすることでUserから直接followedを取得することができるというわけです。

image.png

図にして表すとこのようなイメージです。
実際にコードでは、

has_many :following_user, through: :follower, source: :followed

一覧で取得する際の名前を:following_userと命名し、user.following_userでフォローしている人の一覧を呼び出すことができるように設定しています。
そして、through:では:followerモデル(Relationshipモデル)を通して取得してくださいと書いてますね。ちなみにこの:followerモデルがRelationshipモデルだということがわかるのは、

has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy

User.rbのこの記述で、class_nameで本当はRelationshipモデルなんだけど:followerとして扱ってねと設定しているからでしたね!
そして最後に、sourceでは今回の関連しているモデルを記載してあげます。なので、今回関連しているモデルはfollowedモデルになるのでsource: :followedと記載をしてあげます。
この記述がUserモデルだとわかるのは、Relationshipモデルの

belongs_to :followed, class_name: "User"

本当はUserなんだけど:followedとして扱ってねと設定をしているからでしたね。
image.png
これで@user.following_userと入力するだけで取得する設定が完了しましたね。

ここまできたら、

has_many :follower_user, through: :followed, source: :follower

フォローされている人の一覧もさきほどフォローで考えたことと同じ手順で進めます。
:follower_userで一気に取得できるように命名します。(名前はなんでもおっけい)
今度はフォローされる側になるので、

has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy

こちらのフォローされているアソシエーションの方を使用します。
そして、今回は自分のことをフォローしている人のデータを取得したいので:followerというUserモデルを孫要素として取得したいので、source: :followerとして取得をします。

image.png

これでフォローされる側の設定も完了することができましたね〜〜〜!

最後に

以上がフォローの解説になります。
実際に他の人の記事を読んでもなかなかしっくり来ず、今回この記事を通して自分の理解を深めようと思って書かせていただきました。
実際に、アソシエーションの関係を自分なりに整理してみるとこういうことかという発見が多くありいい勉強になりました。この記事が一人でも多くの方の参考になれば幸いです。
また個々の認識間違ってるよ〜〜などご意見があればコメントでいただけますと幸いです!!

8
2
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
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?