##フォロー機能
特定のユーザーをフォローできるようにし、フォローしたユーザーのマイクロポストをフィードに表示できるようにする。
##Relationshipモデル
###能動関係と受動関係
ユーザーAがユーザーBをフォローしている場合、AにはBをフォローしている(following, follower)という能動関係がある。
逆に、BにはAにフォローされている(followed)という受動関係がある。
この関係を構築するために、Relationshipモデルを作成する。
Relationshipモデルには、フォローしているユーザーのIDを保存するfollower_idと、フォローされているユーザーのIDを保存するfollowed_idカラムがある。
$ rails generate model Relationship follower_id:integer followed_id:integer
Relationshipモデルからは頻繁にユーザーを検索することになるので、インデックスを追加する。
class CreateRelationships < ActiveRecord::Migration[5.0]
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :followed_id
t.timestamps
end
add_index :relationships, :follower_id
add_index :relationships, :followed_id
add_index :relationships, [:follower_id, :followed_id], unique: true
end
end
最後の行は複合キーインデックスと呼ばれ、follower_idとfollowed_idの組み合わせがユニークであり、同じユーザーを2回フォローしたりすることがないようにしている。
$rails db:migrateしておく。
###UserとRelationshipの関連付け/外部キーと能動関係
UserモデルとMicropostモデルの関連付けはhas_manyとbelongs_toを使って実現していた。
Userモデルではhas_many :micropostsとすることで、Railsは対応するMicropostモデルを見つけることができる。
今回は能動関係を表す関連付けを:active_relationshipとしたいのだが、これだとActiveRelationshipモデルを探してしまうので、これがRelationshipモデルであることを明示する必要がある。
また、Micropostモデルではbelongs_to :userとすることで、Railsは対応するUserモデルを見つけることができる。
この時、Micropostモデルにはuser_id属性があるので、これをもとに正確なUserオブジェクトを見つけている。
user_id属性のような、2つのモデルを関連づける属性を外部キー(foreign key)と呼ぶ。
Railsにおける外部キーのデフォルトは<class>_idという形になっており、<class>の部分にはクラス(モデル)名を小文字にしたものが入る。
今回はフォローユーザーを見つけるためにfollower_idを外部キーとして使うのだが、Followerモデルは存在しないので、これが外部キーであることを明示する必要がある。
以上により、UserモデルのRelationshipモデルに対する関連付けは以下のようになる(能動関係)。
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
.
.
.
end
Userモデルは多くの能動関係(active_relationships、ユーザーをフォローしている)を持ち、それはfollower_idによってRelationshipモデルに関連づけられる。
また、ユーザーが削除されたらその関係も削除される(dependent: :destroy)。
次に、RelationshipモデルをUserモデルに関連づける。
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
この関連付けにより、以下のようなメソッドが使えるようになる。
###Relationshipモデルのバリデーション
Relationshipモデルのfollower_id属性とfollowed_id属性に存在性のバリデーションを追加する。
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end
テストも書いておく。
require 'test_helper'
class RelationshipTest < ActiveSupport::TestCase
def setup
@relationship = Relationship.new(follower_id: users(:michael).id,
followed_id: users(:archer).id)
end
test "should be valid" do
assert @relationship.valid?
end
test "should require a follower_id" do
@relationship.follower_id = nil
assert_not @relationship.valid?
end
test "should require a followed_id" do
@relationship.followed_id = nil
assert_not @relationship.valid?
end
end
ここでREDになるのは、自動生成されたRelationshipモデル用のfixtureファイルが原因である。
fixtureファイルを空にすればGREENになる。
##フォロー/被フォローの関連付け(能動関係)
###被フォローユーザー
Userモデルにhas_many throughを使って被フォローユーザー(followed)を関連づけ、被フォローユーザーを取得できるようにする。
フォローユーザー(follower)も被フォローユーザーも共にUserモデルのオブジェクトなので、Userモデル(follower)からRelationshipモデルを経由してUserオブジェクト(followed)を取得するという流れになる。
具体的には以下のコードになる。
has_many :followeds, through: :active_relationships
ユーザーは多くの被フォローユーザー(followeds)を持ち、RailsはRelationshipテーブルのfollowed_id(followedsからsを取った単数形)から各被フォローユーザーを取得する。
ここで、followedsというのは文法的に正しくないので、代わりにfollowingを使うことにする。
(過去分詞の代わりに現在分詞を使うことは、誤解を招く原因になると思うが...)
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
.
.
.
end
source:パラメータを使って、following=followedであることを明示する。
これにより、user.followingとすることで、被フォローユーザーを配列で取得できるようになる。
##フォロー関連のメソッド
ユーザーを簡単にフォローしたりフォロー解除できるように、followやunfollowメソッドを作成する。
また、following?論理値メソッドを作成し、フォロしているかどうかを確認できるようにする。
###メソッドのテスト
テストから書いていく。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "should follow and unfollow a user" do
michael = users(:michael)
archer = users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
end
①michaelがarcherをフォローしていないことをfollowing?メソッドで確認する。
②michaelがarcherをフォローする。
③フォローできていることを確認する。
④フォロー解除する。
⑤フォロー解除できていることを確認する。
###メソッドの実装
各メソッドを実装する。
def feed
.
.
.
end
# ユーザーをフォローする
def follow(other_user)
following << other_user
end
# ユーザーをフォロー解除する
def unfollow(other_user)
active_relationships.find_by(followed_id: other_user.id).destroy
end
# 現在のユーザーがフォローしていたらtrueを返す
def following?(other_user)
following.include?(other_user)
end
private
.
.
.
##フォロー/被フォローの関連付け(受動関係)
###フォローユーザー
Userモデルとactive_relationshipを関連づけ、被フォローユーザーを取得できるようになったので、逆にフォローユーザーを取得できるようにする。
これは能動関係の関連付けとちょうど逆のことをするだけでよい。
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
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
ここでsourceパラメータは不要だが、followedとの対称性を強調するために付けている。
###メソッドテストの追記
この関連付けによってfollowsメソッドを使えるようになり、フォローユーザーを被フォロワーユーザーから取得できるようになったので、テストに追記する。
test "should follow and unfollow a user" do
michael = users(:michael)
archer = users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
assert archer.followers.include?(michael)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
archerをフォローしているユーザーの中に、michaelがいるかどうかをinclude?メソッドで確認している。