0
1

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チュートリアル】第14章 ユーザーをフォローする①

Posted at

はじめに

  • 他のユーザーをフォロー(およびフォロー解除)できるソーシャルな仕組みの追加する
  • フォローしているユーザーの投稿をステータスフィードに表示する機能を追加する

ページ操作の全体的なフロー

  1. あるユーザー(John Calvin)は自分のプロフィールページを最初に表示し(図 14.1)、フォローするユーザーを選択するためにUsersページ(図 14.2)に移動します。
  2. Calvinは2番目のユーザーThomas Hobbes(図 14.3)を表示し、[Follow]ボタンを押してフォローします。
  3. これにより、[Follow]ボタンが[Unfollow]に変わり、Hobbes の[followers]カウントが1人増えます(図 14.4)。
  4. CalvinがHomeページに戻ると、[following]カウントが1人増え、Hobbesのマイクロポストがステータスフィードに表示されるようになっていることがわかります(図 14.5)。

14.1 Relationshipモデル

14.1.1 データモデルの問題(および解決策)

  • Calvin→[followed]→Hobbes
  • Calvin←[following]←Hobbes
  • フォロワーの集合: calvin.following
db/migrate/[timestamp]_create_relationships.rb
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の関連付けを行います。
app/models/user.rb
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を教えてあげる。

app/models/relationship.rb
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のバリデーション

test/models/relationship_test.rb
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
app/models/relationship.rb
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
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?