娯楽開発

娯楽開発

開発は娯楽。遊び心で開発をすすめるブログ。

【Railsチュートリアル】フォロー機能のアソシエーションを解説してみる

みなさん一度くらいユーザーのフォロー機能を実装したくなったことありません?ありますか。奇遇ですね僕もです。

と、いうわけで今回の記事ではフォロー機能の実装とその解説をしたいと思います。

今回は実際のコードを見ながら説明をしていこうと思うので、実際にrailsでコードを書いたことがある人向けの内容になります。

全くコードを書いたことないんだけど!アソシエーションって何やねん!という方、なんとびっくり偶然にも僕の前回の記事がたまたまコードを書かずにアソシエーションの概念について説明しているのでそちらを見ていただければ幸いです。是非。

sado-tech.hateblo.jp

今回の目的

さてさて今回の記事の目的ですが、

  • フォロー機能のためのアソシエーション(とそのコードの意味)の理解

とします。機能や実装方法については調べればこれでもかと出てくるので色々調べてみてください。

また今回の記事ではコードの曖昧さ回避の意味も兼ねて Ruby on Rails チュートリアル:実例を使って Rails を学ぼう (version 5.1)の14章、 14.1.1 データモデルの問題 (および解決策) から 14.1.5 フォロワー についての説明に絞ろうと思います。

railsチュートリアルをやったことがない人にもわかりやすいよう書くつもりですが、もしよくわからない部分があれば本家サイトの方も参照してみてください。

railstutorial.jp

コード

Railsチュートリアルに沿っていればおおよそこんな感じになってるはずです。

# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

# app/models/user.rb
class User < ApplicationRecord
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
end

今回はフォロー機能についての各モデルのアソシエーションを中心に解説するので一部余分なコード(Userモデルのmicropost関連など)は省いています。

解説

それでは早速見ていきましょう。

そもそも

はい、いきなりコードの話から脱線するんですけど、そもそも今回のフォロー機能ではUserモデルからUserモデルに対しての多対多のアソシエーションを行っています。 前回の記事で言う所の「A組の学生」モデルと「B組の学生」モデルがどっちもUserになっている状態です。

つまり、Userモデルは自分自身のインスタンスからその他の自分自身のインスタンスに対してアソシエーションを行わなければならないわけです。カオス。

これを解決する為に、has_many :throughアソシエーション(has_and_belongs_to_manyでも可)を使ってUserモデルの間にRelationshipモデル(中間モデル)を置きます。

前回の記事に習って書くと、今回実現されるモデル構造は以下のようになります。

図1 f:id:sado_tech:20180816233548p:plain

ただ実際にはUserモデルは一つだけなので、今回の記事では以下のような図を使って解説していきたいと思います。

図2 f:id:sado_tech:20180817193439p:plain

ちなみに今回の「フォロー機能」では図のように3つのアソシエーション(×2)が行われています。 ((×2)としたのはそれぞれに対して「フォローしている」アソシエーションと「フォローされている」アソシエーションの2種類が発生するからです。)

図の中の番号はこの後の解説の順番を示しているだけで深い意味はないのでそんなに気にしないでください。

コードの読み方

Relationshipモデル

それではまず比較的シンプルなRelationshipモデルから。図2で言う①にあたる部分です。

# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

図1からもわかるようにRelationshipモデルはUserモデルに対して常に1対1の関連を持っており、かつUserモデルに所属しているので使うアソシエーションはbelongs_toです。

ではこのbelongs_toですが、カンマ(,)区切りで見てみるとそれぞれ二つの情報を取得していることがわかります。

まず一つ目の情報(:follower, :followed)ですが、これらはRelationshipモデルが関連を持つ先のモデルを示します。しかし上の図を見るとRelationshipモデルが関連を持つ相手はUserモデルだけですよね。followerfollowingといったモデルはありません。そこで活きてくるのが二つ目の情報、class_name:です。

class_name:をつけることで、関連先のモデルを参照する際の名前を変更することができます。

上のコードでのbelongs_to :follower, class_name: "User"という記述がありますが、日本語的に意訳すると

「Userモデルに対してfollowerって名前でbelongs_toの関連付けしときますねー」

という意味になります。

なぜこんなことをするのかというと、あとで解説しますが今回実はRelationshipモデルはUserモデルに対して二つの関連付けをしたいんです。それなのに関連先として使える名前が"User"だけだと、二つの関連付けの見分けがつきませんね。そこでclass_nameで本当の名前を指定することで関連先の名前を変更できるというわけです。

この関連付けを図にすると以下のようになります。

図3 f:id:sado_tech:20180818105933p:plain

Userモデル

続いてUserモデルについてです。

# app/models/user.rb
class User < ApplicationRecord
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
end

Relationshipモデルより少し複雑に見えます。いや実際複雑なんですけども。

ここではhas_manyhas_many :throughという大きく二つの関連付けがなされています。ひとつずつ見ていきましょう。

has_many

図2で言う②の部分ですね。以下の一文について考えてみましょう。

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

has_manyはカンマ区切りで見ると四つの情報を取得していることがわかります。このうちclass_name: "Relationship"まではRelationshipモデルのbelongs_toで説明したものと同じ内容になります。

ではforeign_keydependentですが、それぞれ以下のような意味になります。

  • foreign_key 関連先モデルの外部キーを指定する。
  • dependent 自モデルと関連先モデルの依存性を指定する。

foreign_keyについてですが、Relationshipモデルに生成されたインスタンスfollower_idfollowed_idなどいくつかのデータ(カラムといいます)を持ちます。その中で、アソシエーションによって参照するカラムのことをforeign_key(外部キー)として指定しています。

dependentでは自モデルと関連先モデルの依存性を指定します。destroyを指定すると自モデルのインスタンスが削除された際にその関連先モデルのインスタンスも自動的に削除されるようになります。

つまり、ここでのhas_manyアソシエーションを日本語的に意訳すると、

「Relationshipモデルに対してfollower_idを見てactive_relathionshipsって名前でアソシエーションしときますねー。あ、あと関連するUserインスタンスが消えた時は対応するactive_relationshipsインスタンスも消しとくんでよろしくー。」

みたいな感じになります。

この関連付けを図にすると以下のようになります。

図4 f:id:sado_tech:20180818110003p:plain

has_many :through

最後に図2で言う③の部分です。以下の一文について考えてみます。

has_many :following, through: :active_relationships,  source: :followed

ここで設定されている情報は以下のようになっています。

  • through 経由するモデル名を指定する。
  • source 関連先モデル名を指定する。

前回の記事でも触れているんですが、has_many :throughではアソシエーションを行うモデル間を中継する中間モデルが存在します。その中間モデル名をthroughで指定していますね。今回の例だと、has_manyで関連付けしたactive_relationshipsが指定されています。

またこのアソシエーションでも最終的な関連先モデルの名前をfollowingに変更していますが、has_manyで指定していたclass_nameが見当たりません。代わりにsourceで関連先のモデルを指定するんですが、ここで指定するのはUserではなくfollowedであることに注意が必要です。

このアソシエーションによって行う関連付けはUser→Relationship→Userですが、Relationshipモデルでのアソシエーションで説明した通りRelationship→Userではfollowerfollowedの2つの関連付けが行われています。そのような経緯で今回の場合だとsourceではUserではなくfollowerfollowedを指定しなければならないため、followerを指定しているということになります.

つまり、ここでのhas_many :throughアソシエーションを日本語的に意訳すると、

active_relationshipsモデルを経由した上でfollowedモデルに対してfollowingって名前でアソシエーションしときますねー」

といった感じになります。

この関連付けを図にすると以下のようになります。

図5 f:id:sado_tech:20180818110654p:plain

まとめ

最終的に今回のアソシエーションを全て一つの図に表すと以下のようになります。

図6 f:id:sado_tech:20180818110109p:plain

なんとなくでもユーザーをフォローするためのアソシエーションが理解できたでしょうか。

今回の例はアソシエーションを考える上でまだ比較的簡単な方かと思いうので、ぜひいろんなアソシエーションにチャレンジしてみてください。