はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 14章 14.1 Relationshipモデルの演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
動作環境
- 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テーブル
- 自分からフォローする(能動的関係)
- 相手からフォローされる(受動的関係)
どちらも1つのテーブルで実現するため、テーブル名はrelationships
テーブルとなる。
上記を踏まえ、実装する。
【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回以上フォローできないようにする)
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
を付与している。
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
で関連付けを行う。
class Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" end
今回使えるようになったメソッド
演習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
- @relationshipが有効であること
should require a follower_id
- @relationshipのfollower_idにnilを代入
- @relationshipが有効ではないこと
should require a followed_id
- @relationshipのfollowed_idにnilを代入
- @relationshipが有効ではないこと
上記を踏まえて実装する。
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モデルに対してバリデーションを追加する
validates :follower_id, presence: true validates :followed_id, presence: true
【fixture】Relationship用のfixtureを空にする
自動生成されたfixtureでは、Relationshipモデルの複合キーの一意性を満たせないため、 コメントアウトする。
# one: # follower_id: 1 # followed_id: 1 # two: # follower_id: 1 # followed_id: 1
演習1
リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)
該当箇所をコメントアウトする。
# 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の集合である」ことを明示的にする。
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をフォローしていないことを確認
上記を踏まえて実装する。
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関連のメソッドの実装
テストコードで作成したメソッドの中身を実装する。
# ユーザーをフォローする 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
上記を踏まえて実装する。
source: :follower
を省略することも可能。
has_many :followers
と記述した場合、Railsは自動的にfollowersを
単数形のfollowerに変換して、follower_idを探してくれる。
今回は、has_many :following
との類似性を強調するために、
あえて記述している。
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をフォローしていないことを確認
上記を踏まえて実装する。
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
おわりに
ついに最後の章に入りました! これまでのモデルに比べると、多対多の関係となっているため複雑です。 モデル間の構造を意識しながら、これからもプログラムを書いていきます。