はじめに
- 他のユーザーをフォロー(およびフォロー解除)できるソーシャルな仕組みの追加する
- フォローしているユーザーの投稿をステータスフィードに表示する機能を追加する
ページ操作の全体的なフロー
- あるユーザー(John Calvin)は自分のプロフィールページを最初に表示し(図 14.1)、フォローするユーザーを選択するためにUsersページ(図 14.2)に移動します。
- Calvinは2番目のユーザーThomas Hobbes(図 14.3)を表示し、[Follow]ボタンを押してフォローします。
- これにより、[Follow]ボタンが[Unfollow]に変わり、Hobbes の[followers]カウントが1人増えます(図 14.4)。
- CalvinがHomeページに戻ると、[following]カウントが1人増え、Hobbesのマイクロポストがステータスフィードに表示されるようになっていることがわかります(図 14.5)。
14.1 Relationshipモデル
14.1.1 データモデルの問題(および解決策)
- Calvin→[followed]→Hobbes
- Calvin←[following]←Hobbes
- フォロワーの集合: calvin.following
class CreateRelationships < ActiveRecord::Migration[6.0]
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :followed_id
t.timestamps
end
add_index :relationships, :follower_id
# フォローしているidのインデックス
add_index :relationships, :followed_id
# フォローされているidのインデックス
add_index :relationships, [:follower_id, :followed_id], unique: true
# あるユーザーが同じユーザーを2回以上フォローすることを防ぐ仕組み
end
end
演習 1
図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
[2, 7, 10, 8]
演習 2
図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。
[user_id:1, name:Michael Hartl, email:mhartl@example.com]
[1]
14.1.2 User/Relationshipの関連付け
- フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行います。
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships,
class_name: "Relationship",
# ActiveRelationshipを探してしまいRelationshipモデルを見つけることができない
# class_name: でクラス名を明示する
foreign_key: "follower_id",
# foreign_key(外部キー)他のモデルに紐付けるためのカラム
dependent: :destroy
# このデータが削除された場合に関連するデータも一緒に削除する
.
.
.
end
Relationship
と紐付けているid:follower_id
を教えてあげる。
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
class_name: "User"
を記述しない場合、デフォルトだとfollower_id
を探してしまう(follower_idは実装していない)
followerカラムはUserクラスと紐付けるためにあるのでclass_name: "User"
を記述して、User
を探してもらうようにする。
演習 1
コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。
>> user = User.first
# 1人目のユーザーを変数userに代入
(4.1ms) SELECT sqlite_version(*)
User Load (3.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-03-16 03:01:20", updated_at: "2021-03-16 03:01:20", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$VFPF70fqU0mWCdYrpnPI5uRjn1yQePv61n5WYjNlvvo...", activated: true, activated_at: "2021-03-16 03:01:19", reset_digest: nil, reset_sent_at: nil>
>> other_user = User.second
# 2人目のユーザーを変数other_userに代入
User Load (1.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Larry Emmerich", email: "example-1@railstutorial.org", created_at: "2021-03-16 03:01:22", updated_at: "2021-03-16 03:01:22", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$GdyVpNHiPiHM1577YSHvt.amKLpH5JCzMBn25oJ5zMt...", activated: true, activated_at: "2021-03-16 03:01:21", reset_digest: nil, reset_sent_at: nil>
>> user.active_relationships.create(followed_id: other_user.id)
(0.2ms) begin transaction
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Relationship Create (13.7ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2021-03-16 08:30:39.786757"], ["updated_at", "2021-03-16 08:30:39.786757"]]
(7.8ms) commit transaction
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2021-03-16 08:30:39", updated_at: "2021-03-16 08:30:39">
演習 2
先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。
演習1を参照
14.1.3 Relationshipのバリデーション
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
# follower_idが存在するか検証するテスト
@relationship.follower_id = nil
# follower_idにnilを代入
assert_not @relationship.valid?
# @relationshipは無効である
end
test "should require a followed_id" do
# followed_idが存在するか検証するテスト
@relationship.followed_id = nil
# followed_idにnilを代入
assert_not @relationship.valid?
# @relationshipは無効である
end
end
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
# follower_idが存在するとtrue
validates :followed_id, presence: true
# followed_idが存在するとtrue
end
演習 1
リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
GREENでした。
14.1.4 フォローしているユーザー
演習 1
コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
>> michael = User.first
(4.5ms) SELECT sqlite_version(*)
User Load (3.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-03-16 03:01:20", updated_at: "2021-03-16 03:01:20", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$VFPF70fqU0mWCdYrpnPI5uRjn1yQePv61n5WYjNlvvo...", activated: true, activated_at: "2021-03-16 03:01:19", reset_digest: nil, reset_sent_at: nil>
>> archer = User.second
User Load (3.4ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Larry Emmerich", email: "example-1@railstutorial.org", created_at: "2021-03-16 03:01:22", updated_at: "2021-03-16 03:01:22", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$GdyVpNHiPiHM1577YSHvt.amKLpH5JCzMBn25oJ5zMt...", activated: true, activated_at: "2021-03-16 03:01:21", reset_digest: nil, reset_sent_at: nil>
>> michael.unfollow(archer)
Relationship Load (1.9ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
(0.1ms) begin transaction
Relationship Destroy (11.0ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 1]]
(12.0ms) commit transaction
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2021-03-16 08:30:39", updated_at: "2021-03-16 08:30:39">
>> michael.following?(archer)
User Exists? (1.6ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> false
>> michael.follow(archer)
(0.1ms) begin transaction
User Load (1.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Relationship Create (7.6ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2021-03-16 10:27:01.323420"], ["updated_at", "2021-03-16 10:27:01.323420"]]
(11.6ms) commit transaction
User Load (1.5ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, name: "Larry Emmerich", email: "example-1@railstutorial.org", created_at: "2021-03-16 03:01:22", updated_at: "2021-03-16 03:01:22", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$GdyVpNHiPiHM1577YSHvt.amKLpH5JCzMBn25oJ5zMt...", activated: true, activated_at: "2021-03-16 03:01:21", reset_digest: nil, reset_sent_at: nil>]>
>> michael.following?(archer)
User Exists? (3.4ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> true
>> michael.unfollow(archer)
Relationship Load (2.2ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
(0.1ms) begin transaction
Relationship Destroy (7.0ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 2]]
(8.8ms) commit transaction
=> #<Relationship id: 2, follower_id: 1, followed_id: 2, created_at: "2021-03-16 10:27:01", updated_at: "2021-03-16 10:27:01">
>> michael.following?(archer)
User Exists? (1.9ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 1], ["id", 2], ["LIMIT", 1]]
=> false
演習 2
先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。
演習1参照
14.1.5 フォロワー
- user.followersメソッドを追加する
演習 1
コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
>> user = User.first
(0.1ms) begin transaction
User Load (2.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-03-16 03:01:20", updated_at: "2021-03-16 03:01:20", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$VFPF70fqU0mWCdYrpnPI5uRjn1yQePv61n5WYjNlvvo...", activated: true, activated_at: "2021-03-16 03:01:19", reset_digest: nil, reset_sent_at: nil>
>> user2 = User.second
User Load (0.5ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Larry Emmerich", email: "example-1@railstutorial.org", created_at: "2021-03-16 03:01:22", updated_at: "2021-03-16 03:01:22", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$GdyVpNHiPiHM1577YSHvt.amKLpH5JCzMBn25oJ5zMt...", activated: true, activated_at: "2021-03-16 03:01:21", reset_digest: nil, reset_sent_at: nil>
>> user3 = User.third
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 2]]
=> #<User id: 3, name: "Marcy Morissette II", email: "example-2@railstutorial.org", created_at: "2021-03-16 03:01:22", updated_at: "2021-03-16 03:01:22", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$flofQ1ScEzY6kQKAXuQxS.YsUtSc9n2RjnB1wGAOlzb...", activated: true, activated_at: "2021-03-16 03:01:22", reset_digest: nil, reset_sent_at: nil>
>> user2.follow(user)
(0.2ms) SAVEPOINT active_record_1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
Relationship Create (12.7ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 2], ["followed_id", 1], ["created_at", "2021-03-16 11:08:34.963873"], ["updated_at", "2021-03-16 11:08:34.963873"]]
(0.4ms) RELEASE SAVEPOINT active_record_1
User Load (0.5ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-03-16 03:01:20", updated_at: "2021-03-16 03:01:20", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$VFPF70fqU0mWCdYrpnPI5uRjn1yQePv61n5WYjNlvvo...", activated: true, activated_at: "2021-03-16 03:01:19", reset_digest: nil, reset_sent_at: nil>]>
>> user3.follow(user)
(0.2ms) SAVEPOINT active_record_1
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
Relationship Create (0.5ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 3], ["followed_id", 1], ["created_at", "2021-03-16 11:08:41.649913"], ["updated_at", "2021-03-16 11:08:41.649913"]]
(0.2ms) RELEASE SAVEPOINT active_record_1
User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 3], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-03-16 03:01:20", updated_at: "2021-03-16 03:01:20", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$VFPF70fqU0mWCdYrpnPI5uRjn1yQePv61n5WYjNlvvo...", activated: true, activated_at: "2021-03-16 03:01:19", reset_digest: nil, reset_sent_at: nil>]>
>> user.followers.map(&:id)
User Load (0.3ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> [2, 3]
演習 2
上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
>> user.followers.count
(0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
=> 2
演習 3
user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
>> user.followers.to_a.count
=> 2