紙一重の積み重ね

35歳のエンジニアがなれる最高の自分を目指して、学んだことをこつこつ情報発信するブログです。

【13章】Ruby on Railsチュートリアル演習まとめ&解答例【13.1 Micropostモデル】

はじめに

Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 13章 13.1 Micropostモデルの演習まとめ&解答例です。。

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

動作環境

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

13.1.1 基本的なモデル

本章での学び

以下の属性を持つmicropostsテーブルを作成する。

image

【Model】Micropostモデルの生成

user:referencesを付与することで、ユーザーと1対1の関係であることを示す、belongs_to:userがモデルの中に付与される。

yokoyan:~/workspace/sample_app (user-microposts) $ rails generate model Micropost content:text user:references
Running via Spring preloader in process 1727
Expected string default value for '--jbuilder'; got true (boolean)
      invoke  active_record
      create    db/migrate/20170719214241_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml

【DB】マイグレーションファイルの編集

自動生成されたマイグレーションファイルに、インデックスを追加する。 user_idとcreated_atによる複合キーを生成する。

class CreateMicroposts < ActiveRecord::Migration[5.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails db:migrate
== 20170719214241 CreateMicroposts: migrating =================================
-- create_table(:microposts)
   -> 0.0074s
-- add_index(:microposts, [:user_id, :created_at])
   -> 0.0010s
== 20170719214241 CreateMicroposts: migrated (0.0087s) ========================

演習1

RailsコンソールでMicropost.newを実行し、インスタンスを変数micropostに代入してください。その後、user_idに最初のユーザーのidを、contentに “Lorem ipsum” をそれぞれ代入してみてください。この時点では、 micropostオブジェクトのマジックカラム (created_atとupdated_at) には何が入っているでしょうか?

?> user = User.first
  User Load (0.2ms)  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-06-20 04:07:12", updated_at: "2017-07-19 04:04:54", password_digest: "$2a$10$noNSSwFRwOmZj.z7lvNt0u8Y2miZecow0uxK.cLpJZ0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: "2017-07-19 04:04:20">
>> micropost = Micropost.new
=> #<Micropost id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil>
>> 
?> micropost.user_id = user.id
=> 1
>> 
?> micropost.content = "Lorem ipsum"
=> "Lorem ipsum"
>> 
?> micropost.created_at
=> nil
>> 
?> micropost.updated_at
=> nil

演習2

先ほど作ったオブジェクトを使って、micropost.userを実行してみましょう。どのような結果が返ってくるでしょうか? また、micropost.user.nameを実行した場合の結果はどうなるでしょうか?

?> micropost.user
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-06-20 04:07:12", updated_at: "2017-07-19 04:04:54", password_digest: "$2a$10$noNSSwFRwOmZj.z7lvNt0u8Y2miZecow0uxK.cLpJZ0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: "2017-07-19 04:04:20">
>> micropost.user.name
=> "Example User"

演習3

先ほど作ったmicropostオブジェクトをデータベースに保存してみましょう。この時点でもう一度マジックカラムの内容を調べてみましょう。今度はどのような値が入っているでしょうか?

?> micropost.save
   (0.1ms)  begin transaction
  SQL (2.4ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", 2017-07-23 21:30:50 UTC], ["updated_at", 2017-07-23 21:30:50 UTC]]
   (10.2ms)  commit transaction
=> true
>> 
?> micropost.created_at
=> Sun, 23 Jul 2017 21:30:50 UTC +00:00
>> micropost.updated_at
=> Sun, 23 Jul 2017 21:30:50 UTC +00:00

13.1.2 Micropostのバリデーション

本章での学び

【test】新しいmicropostの有効性に対するテスト

  def setup
    @user = users(:michael)
    # このコードは慣習的に正しくない
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end

  test "should be valid" do
    assert @micropost.valid?
  end

  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end

【Model】micropostモデルにuser_idのバリデーションを追加

user_idに対して、「存在性 (Presence)」のバリデーションを追加する。

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
end

動作確認

現時点でテストがgreenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test:models
Started with run options --seed 38876

  13/13: [===================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.55010s
13 tests, 22 assertions, 0 failures, 0 errors, 0 skips

【test】contentに対するバリデーションのテスト

content属性が存在すること、最大140文字であることの検証を追加する。

  test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
  end
  
  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end

【Model】micropostモデルにcontent属性のバリデーションを追加

content属性に対して、「存在性 (Presence)」のバリデーションを追加する。

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

演習1

Railsコンソールを開き、user_idとcontentが空になっているmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

user_id属性とcontent属性にブランクが許可されていないことを確認。

?> micropost = Micropost.new
=> #<Micropost id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil>
>> 
?> micropost.valid?
=> false
?> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["can't be blank"]}

演習2

コンソールを開き、今度はuser_idが空でcontentが141文字以上のmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

content属性は最大140文字であることを確認。

?> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["can't be blank"]}
>> 
?> 
?> micropost.content = "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> micropost.valid?
=> false
>> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["is too long (maximum is 140 characters)"]}

13.1.3 User/Micropostの関連付け

本章での学び

【Model】マイクロポストは1人のユーザーに属する

Micropost側から見ると、常に1人のユーザーに所属する。

image.png

belongs_to :userで関連付けを表すことができる。

class Micropost < ApplicationRecord
  belongs_to :user

【Model】ユーザーがマイクロポストを複数所有する(has_many)

User側から見ると、1人のユーザーが複数のMicropostを持つ。

image.png

has_many :micropostで関連付けを表すことができる。 micropostではなく、複数形のmicropostsであることに注意。

class User < ApplicationRecord
  has_many :microposts

【Test】慣習的に正しいコードを作成する

micropostのインスタンス変数の生成の仕方を修正する。 userに紐づいた、新しいmicropostオブジェクトを生成する。

  def setup
    @user = users(:michael)
    # このコードは慣習的に正しくない
    # @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
    # このコードは慣習的に正しい
    @micropost = @user.microposts.build(content: "Lorem ipsum")
  end

テストを実行し、greenになることを確認。

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 2466
Started with run options --seed 4897

  51/51: [========================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.85601s
51 tests, 229 assertions, 0 failures, 0 errors, 0 skips

演習1

データベースにいる最初のユーザーを変数userに代入してください。そのuserオブジェクトを使ってmicropost = user.microposts.create(content: “Lorem ipsum”)を実行すると、どのような結果が得られるでしょうか?

データベースに保存される。

>> user = User.first
  User Load (0.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: "2017-06-20 04:07:12", updated_at: "2017-07-19 04:04:54", password_digest: "$2a$10$noNSSwFRwOmZj.z7lvNt0u8Y2miZecow0uxK.cLpJZ0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: "2017-07-19 04:04:20">
>> micropost = user.microposts.create(content: "Lorem ipsum")
   (0.2ms)  begin transaction
  SQL (3.0ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", 2017-07-24 04:17:44 UTC], ["updated_at", 2017-07-24 04:17:44 UTC]]
   (12.0ms)  commit transaction
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-24 04:17:44", updated_at: "2017-07-24 04:17:44">

演習2

先ほどの演習課題で、データベース上に新しいマイクロポストが追加されたはずです。user.microposts.find(micropost.id)を実行して、本当に追加されたのかを確かめてみましょう。また、先ほど実行したmicropost.idの部分をmicropostに変更すると、結果はどうなるでしょうか?

得られる結果は変わらないが、警告が出る。

?> user.microposts.find(micropost.id)
  Micropost Load (0.4ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? LIMIT ?  [["user_id", 1], ["id", 2], ["LIMIT", 1]]
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-24 04:17:44", updated_at: "2017-07-24 04:17:44">
>> 
?> user.microposts.find(micropost)
DEPRECATION WARNING: You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`. (called from irb_binding at (irb):6)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? LIMIT ?  [["user_id", 1], ["id", 2], ["LIMIT", 1]]
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-24 04:17:44", updated_at: "2017-07-24 04:17:44">

演習3

*user == micropost.userを実行した結果はどうなるでしょうか? また、user.microposts.first == micropost を実行した結果はどうなるでしょうか? それぞれ確認してみてください。 *

どちらもtrueになる。

?> user == micropost.user
=> true
?> 
?> user.microposts.first == micropost
=> false

13.1.4 マイクロポストを改良する

本章での学び

default scopeによるデータの並び替え

default scopeを使って、作成時間の降順の結果を得られるようにする。

【Test】マイクロポストを降順で得るテスト
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
【Fixture】マイクロポストのサンプルデータを定義する
orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: http://tauday.com"
  created_at: <%= 3.years.ago %>

cat_video:
  content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>
【Model】default_scopeで作成日の降順に並び替える

ラムダ式を使って、ブロック内で作成日時created_atの降順:descを指定する。

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

ラムダ式

callメソッドが呼ばれた際に、ブロックの中身を評価する。

?> -> { puts "foo" }
=> #<Proc:0x00000003005868@(irb):5 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil

Dependent: destroy

ユーザーが破棄された場合、ユーザーのマイクロポストも同時に破棄する。

【Model】マイクロポストはユーザーと一緒に破棄されることを保証する

has_manyメソッドに、dependent: :destroyオプションを追加する。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy

【Test】Dependent: destroyのテスト

以下のテストを実装する。

  • ユーザーをDBに保存する
  • ユーザーに紐付いたマイクロポストを生成する
  • マイクロポストの数が−1されていることを確認する
    • ユーザーを削除する
  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end

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

yokoyan:~/workspace/sample_app (user-microposts) $ rails test
Running via Spring preloader in process 1892
Started with run options --seed 54679

  53/53: [===================================================================================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.04239s
53 tests, 231 assertions, 0 failures, 0 errors, 0 skips

演習1

Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。

降順と昇順の結果を比較するため、一致しない。

>> Micropost.first.created_at == Micropost.last.created_at
  Micropost Load (2.7ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> false
>> 
?> Micropost.first.created_at
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> Mon, 24 Jul 2017 04:17:44 UTC +00:00
>> 
?> Micropost.last.created_at
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> Sun, 23 Jul 2017 21:30:50 UTC +00:00

演習2

Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか? ヒント: それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。

Micropost.firstは、ORDER BYの並び順にDESC(降順)が指定されている。 つまり、一番新しいマイクロポストから表示される。

Micropost.lastは、ORDER BYの並び順にASC(昇順)が指定されている。 つまり、一番古いマイクロポストから表示される。

?> Micropost.first
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-24 04:17:44", updated_at: "2017-07-24 04:17:44">
>> 
?> Micropost.last
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-23 21:30:50", updated_at: "2017-07-23 21:30:50">

演習3

データベース上の最初のユーザーを変数userに代入してください。そのuserオブジェクトが最初に投稿したマイクロポストのidはいくつでしょうか? 次に、destroyメソッドを使ってそのuserオブジェクトを削除してみてください。削除すると、そのuserに紐付いていたマイクロポストも削除されていることをMicropost.findで確認してみましょう。

DB上の最初のユーザーには、マイクロポストが2つ紐付いており、 ユーザーを削除すると、マイクロポストも削除されることを確認。

ユーザーを削除して、Micropost.findを実行するとraise_record_not_found_exceptionが発生する。

yokoyan:~/workspace/sample_app (user-microposts) $ rails console --sandbox
Running via Spring preloader in process 2512
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.2ms)  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-06-20 04:07:12", updated_at: "2017-07-19 04:04:54", password_digest: "$2a$10$noNSSwFRwOmZj.z7lvNt0u8Y2miZecow0uxK.cLpJZ0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: "2017-07-19 04:04:20">
>> 
?> user.microposts
=> #<ActiveRecord::Associations::CollectionProxy [#<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-24 04:17:44", updated_at: "2017-07-24 04:17:44">, #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2017-07-23 21:30:50", updated_at: "2017-07-23 21:30:50">]>
>> 
?> user.destroy
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.4ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 2]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 1]]
  SQL (0.1ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2017-06-20 04:07:12", updated_at: "2017-07-19 04:04:54", password_digest: "$2a$10$noNSSwFRwOmZj.z7lvNt0u8Y2miZecow0uxK.cLpJZ0...", remember_digest: nil, admin: true, activation_digest: "$2a$10$ufdL0hGRXuEm9biMP3H0deF99cC.BpPrrobGbyL4FQ/...", activated: true, activated_at: "2017-06-20 04:07:12", reset_digest: nil, reset_sent_at: "2017-07-19 04:04:20">
>> 
?> Micropost.find(1)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["id", 1], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find Micropost with 'id'=1
        from /usr/local/rvm/gems/ruby-2.3.0/gems/activerecord-5.0.0.1/lib/active_record/relation/finder_methods.rb:357:in `raise_record_not_found_exception!'

おわりに

Userモデルに加えて、Micropostモデルが登場し、 belongs_toとhas_manyを使うことで複数のモデルを簡単に関連付けることができました。 Railsは本当に便利ですね。