Help us understand the problem. What is going on with this article?

35歳だけどRailsチュートリアルやってみた。[第4版 14章 14.1 Relationshipモデル まとめ&解答例]

More than 3 years have passed since last update.

はじめに

最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。

このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る35歳定年説)と、
新しいことに挑戦したいという思いから、
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版を学習中です。
業務で使うのはもっぱらJavaなのですが、Rails楽しいですね。

これまでEvernoteに記録していましたが、ソースコードの貼付けに限界を感じたため、
Qiitaで自分が学習した結果をアウトプットしていきます。

個人の解答例なので、誤りがあればご指摘ください。

動作環境

  • cloud9
  • ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
  • Rails 5.0.0.1

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

本章での学び

【DB設計】各モデルの考え方

ユーザー、ユーザーがフォローしているユーザー、能動的関係の3つを設計する。

ユーザー:userテーブル
ユーザーがフォローしているユーザー:user.following(userテーブルの集合)
能動的関係:active_relationshipsテーブル

image

  • 自分からフォローする(能動的関係)
  • 相手からフォローされる(受動的関係)

どちらも1つのテーブルで実現するため、テーブル名はrelationshipsテーブルとなる。

image

上記を踏まえ、実装する。

【DB】マイグレーションファイルの作成

rails generateで、relationshipsテーブルの
マイグレーションファイルを自動生成する。

yokoyan:~/workspace/sample_app (following-users) $ rails generate model Relationship follower_id:integer followed_id:integer
^[Running via Spring preloader in process 3357
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170823121831_create_relationships.rb
      create    app/models/relationship.rb
      invoke    test_unit
      create      test/models/relationship_test.rb
      create      test/fixtures/relationships.yml

【DB】マイグレーションファイルにインデックスを追加

自動生成したマイグレーションファイルに、インデックスを追加する。
[:follower_id, :followed_id], unique: trueは複合キー。
2つのidの組み合わせが必ずユニークであることを保証する。
(同じユーザーを2回以上フォローできないようにする)

/sample_app/db/migrate/20170823121831_create_relationships.rb
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

【DB】マイグレーションの実行

relationshipsテーブルを作成する。

yokoyan:~/workspace/sample_app (following-users) $ rails db:migrate
== 20170823121831 CreateRelationships: migrating ==============================
-- create_table(:relationships)
   -> 0.0094s
== 20170823121831 CreateRelationships: migrated (0.0095s) =====================

演習1

図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。

id=1がフォローしているユーザーの配列が取得できる。
(id=2,7,8,10のユーザー配列が取得できる)

演習2

図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

id=2のユーザーは既にフォローしているのでフォローできない(またはフォローが解除される)
id=1がフォローしているユーザーの配列が取得できる。

14.1.2 User/Relationshipの関連付け

本章での学び

【model】能動的関係に対して1対多(has_many)の関連付けを実装する

userモデルをリファクタリングする。
userモデルとrelationshipsモデルは1対多の関係となる。
能動的関係であることを示すために、has_many :active_relationshipと名前をつけている。
明示的にクラス名と、外部キーの名称を指定している。

また、userを削除した場合、user間のrelationshipも削除されるように、
dependent :destroyを付与している。

/sample_app/app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy

【model】リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加する

relationshipモデルと、フォローされているuserモデル(またはフォローしているuserモデル)は1対1となるため、belongs_toで関連付けを行う。

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

今回使えるようになったメソッド

image

演習1

コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください。

最初のユーザーが2人目のユーザーをフォローする。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 2424
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> user = User.first
  User Load (0.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: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> other_user = User.second
  User Load (0.4ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> active_relationship = user.active_relationships.create(followed_id: other_user.id)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.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]]
  SQL (0.5ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-24 10:52:28 UTC], ["updated_at", 2017-08-24 10:52:28 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-24 10:52:28", updated_at: "2017-08-24 10:52:28">

演習2

先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

active_relationship.followedは、自分がフォローしているユーザを返す。
つまり、user_id=1のユーザが、user_id=2のユーザをフォローしているので、User id:2が返ってくる。

active_relationship.followerは、フォロワーを返す。
つまり、user_id=2のユーザのフォロワーである、User id:1が返ってくる。

?> active_relationship.followed
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> active_relationship.follower
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>

14.1.3 Relationshipのバリデーション

本章での学び

【test】Relationshipモデルのバリデーション

  • setup

    • Relationshipモデルを生成する(followed_id: michael,follower_id: archer)
    • インスタンス変数@relationshipに代入する
  • should be valid

  • should require a follower_id

  • should require a followed_id

上記を踏まえて実装する。

/sample_app/test/models/relationship_test.rb
  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

【model】 Relationshipモデルに対してバリデーションを追加する

/sample_app/app/models/relationship.rb
  validates :follower_id, presence: true
  validates :followed_id, presence: true

【fixture】Relationship用のfixtureを空にする

自動生成されたfixtureでは、Relationshipモデルの複合キーの一意性を満たせないため、
コメントアウトする。

/sample_app/test/fixtures/relationships.yml
# one:
#   follower_id: 1
#   followed_id: 1

# two:
#   follower_id: 1
#   followed_id: 1

演習1

リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)

該当箇所をコメントアウトする。

/sample_app/app/models/relationship.rb
  # validates :follower_id, presence: true
  # validates :followed_id, presence: true

コメントアウトしてもテスト結果がgreenになることを確認。

14.1.4 フォローしているユーザー

本章での学び

【model】 Userモデルにfollowingの関連付けを追加

多対多の関係性を表すために、has_many throughを使う。
Railsは、デフォルトでは、has_manyに指定された単数形のモデル名に対応する外部キーを探す。
そのため、:sourceパラメータを使って、「following配列の元はfollowed_idの集合である」ことを明示的にする。

/sample_app/app/models/user.rb
  has_many :following, through: :active_relationships, source: :followed

【test】following関連のメソッドのテスト

  • should follow and unfollow a user
    • fixtureからmichaelユーザ情報を取得
    • fixtureからarcherユーザ情報を取得
    • michaelが、archerをフォローしていないことを確認
    • michaelが、archerをフォローする
    • michaelが、archerをフォローしていることを確認
    • michaelが、archerをフォロー解除する
    • michaelが、archerをフォローしていないことを確認

上記を踏まえて実装する。

/sample_app/test/models/user_test.rb
  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

【model】following関連のメソッドの実装

テストコードで作成したメソッドの中身を実装する。

/sample_app/app/models/user.rb
  # ユーザーをフォローする
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  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

テスト実行

テスト結果がgreenになることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 3177
Started with run options --seed 22441

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only                                                       ] 9% Time: 00:00:01,  ETA: 00:00:16
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  63/63: [=====================================================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.80719s
63 tests, 268 assertions, 0 failures, 0 errors, 0 skips

演習1

コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。

実行結果は以下の通り。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 4373
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>>
?> michael = User.first
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>>
?> archer = User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>>
?> michael.following?(archer)
  User Exists (0.3ms)  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)  SAVEPOINT active_record_1
  User Load (0.4ms)  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]]
  SQL (2.0ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", 2017-08-26 00:55:42 UTC], ["updated_at", 2017-08-26 00:55:42 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-26 00:55:42", updated_at: "2017-08-26 00:55:42">
>> 
?> michael.following?(archer)
  User Exists (0.3ms)  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 (0.3ms)  SELECT  "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ?  [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]]
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.4ms)  DELETE FROM "relationships" WHERE "relationships"."id" = ?  [["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2017-08-26 00:55:42", updated_at: "2017-08-26 00:55:42">
>> 
?> michael.following?(archer)
  User Exists (0.2ms)  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 フォロワー

本章での学び

フォロワー機能を実装する。

【model】受動的関係のデータモデルの考え方

relationshipsテーブルを使う。
Michael(id=1)は、3人のユーザのにフォローされている。(id=3,2,9)
followed_id=1と、follower_id=3,2,9

image

上記を踏まえて実装する。

source: :followerを省略することも可能。
has_many :followersと記述した場合、Railsは自動的にfollowersを
単数形のfollowerに変換して、follower_idを探してくれる。

今回は、has_many :followingとの類似性を強調するために、
あえて記述している。

/sample_app/app/models/user.rb
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

【test】followersに対するテスト

  • should follow and unfollow a user
    • fixtureからmichaelユーザ情報を取得
    • fixtureからarcherユーザ情報を取得
    • michaelが、archerをフォローしていないことを確認
    • michaelが、archerをフォローする
    • michaelが、archerをフォローしていることを確認
    • archerのフォロワーに、michaelが存在することを確認
    • michaelが、archerをフォロー解除する
    • michaelが、archerをフォローしていないことを確認

上記を踏まえて実装する。

/sample_app/test/models/user_test.rb
  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.follower.include?(michael)
    michael.unfollow(archer)
    assert_not michael.following?(archer)
  end

動作確認

テストがgreenになることを確認。

yokoyan:~/workspace/sample_app (following-users) $ rails test
Running via Spring preloader in process 5854
Started with run options --seed 4043

DEPRECATION WARNING: ActionDispatch::IntegrationTest HTTP request methods will accept only=======================                               ] 77% Time: 00:00:03,  ETA: 00:00:01
the following keyword arguments in future Rails versions:
params, headers, env, xhr, as

Examples:

get '/profile',
  params: { id: 1 },
  headers: { 'X-Extra-Header' => '123' },
  env: { 'action_dispatch.custom' => 'custom' },
  xhr: true,
  as: :json
 (called from block (2 levels) in <class:MicropostsInterfaceTest> at /home/ubuntu/workspace/sample_app/test/integration/microposts_interface_test.rb:27)
  63/63: [=====================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.26879s
63 tests, 269 assertions, 0 failures, 0 errors, 0 skips

演習1

コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?

最初のユーザから、4番目までのユーザを作成する。

yokoyan:~/workspace/sample_app (following-users) $ rails console --sandbox
Running via Spring preloader in process 6034
Loading development environment in sandbox (Rails 5.0.0.1)
Any modifications you make will be rolled back on exit
>> 
?> user = User.first
  User Load (0.6ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-07-26 21:31:59", updated_at: "2017-07-26 21:31:59", password_digest: "$2a$10$hr3puKUHMOdKoV.IkakM0uxzu3mOW./qM63oJ6Xq3q9...", remember_digest: nil, admin: true, activation_digest: "$2a$10$g6M8JTC1B48HIxzpi6iy8.qBKbQsxyaRos1ldkd2q9n...", activated: true, activated_at: "2017-07-26 21:31:59", reset_digest: nil, reset_sent_at: nil>
>> 
?> user2 = User.second
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Chester Greenfelder III", email: "example-1@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$OEftiG18yO2djfYF/Qd2yODa4aYbgFbLpgWRhX9zw/....", remember_digest: nil, admin: false, activation_digest: "$2a$10$tnRwE2WFuUJiQ4BlKEXbne1h2Ejr.8LrUPA7FY.WQMw...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
u?> user3 = User.third
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 2]]
=> #<User id: 3, name: "Chad Runolfsdottir", email: "example-2@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$JobCCI0QOmLfs6UN4SykdeCm4qGJv29oIWahAIxIAuR...", remember_digest: nil, admin: false, activation_digest: "$2a$10$ln97hpFaoM4p8escgCyvVO30Sh.Z7h2ZQoGPCDMfcao...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>
>> 
?> user4 = User.fourth
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 3]]
=> #<User id: 4, name: "Alexandro Lemke", email: "example-3@railstutorial.org", created_at: "2017-07-26 21:32:00", updated_at: "2017-07-26 21:32:00", password_digest: "$2a$10$9bcHizLobKZITlg1egPVy.z0R1AUN800245UXxH71Ct...", remember_digest: nil, admin: false, activation_digest: "$2a$10$lQQRPpzraXZ/uQ/x8FFhyunDpLaLlpMX6gB5O2b4fML...", activated: true, activated_at: "2017-07-26 21:32:00", reset_digest: nil, reset_sent_at: nil>

3人のユーザが最初のユーザーをフォローする。

>> 
?> user2.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.4ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.9ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 2], ["followed_id", 1], ["created_at", 2017-08-26 04:05:32 UTC], ["updated_at", 2017-08-26 04:05:32 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 2, followed_id: 1, created_at: "2017-08-26 04:05:32", updated_at: "2017-08-26 04:05:32">
>> 
?> user3.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.5ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 3], ["followed_id", 1], ["created_at", 2017-08-26 04:05:41 UTC], ["updated_at", 2017-08-26 04:05:41 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 2, follower_id: 3, followed_id: 1, created_at: "2017-08-26 04:05:41", updated_at: "2017-08-26 04:05:41">
>> 
?> user4.follow(user)
   (0.1ms)  SAVEPOINT active_record_1
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  SQL (0.3ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 4], ["followed_id", 1], ["created_at", 2017-08-26 04:05:53 UTC], ["updated_at", 2017-08-26 04:05:53 UTC]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 3, follower_id: 4, followed_id: 1, created_at: "2017-08-26 04:05:53", updated_at: "2017-08-26 04:05:53">

最初のユーザーのフォロー数を確認する。
3人のユーザからフォローされていることを確認。

?> user.followers.map(&:id)
  User Load (0.4ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [2, 3, 4]

演習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]]
=> 3

演習3

user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

user.followers.to_a.countでも、同じく3が返ってくる。
100万人のフォロワーがいた場合、100万人分の配列を生成してその数を返す。
配列を生成する分、実行コストがかかると思われる。

一方、user.followers.countは、Railsは高速化のためにDB内で合計を計算する。

?> user.followers.to_a.count
=> 3

おわりに

ついに最後の章に入りました!
これまでのモデルに比べると、多対多の関係となっているため複雑です。
モデル間の構造を意識しながら、これからもプログラムを書いていきます。

yokoyan
SIerで働くエンジニアです。自社WEBサービスの開発・AWSのインフラ構築を行っています。なれる最高のフルスタックエンジニアを目指して、RailsやAWSをこつこつ学習中。 本を10冊出すことが夢です。 「やさしいT-SQL入門」(翔泳社)および、「基礎からわかる PL/SQL」(シーアンドアール研究所)の著者。
http://www.yokoyan.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした