LoginSignup
6
1

More than 5 years have passed since last update.

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

Posted at

はじめに

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

このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る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

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による複合キーを生成する。

/sample_app/db/migrate/20170719214241_create_microposts.rb
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の有効性に対するテスト

/sample_app/test/models/micropost_test.rb
  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文字であることの検証を追加する。

/sample_app/test/models/micropost_test.rb
  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)」のバリデーションを追加する。

/sample_app/app/models/micropost.rbh
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であることに注意。

/sample_app/app/models/user.rb
class User < ApplicationRecord
  has_many :microposts

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

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

/sample_app/test/models/micropost_test.rb
  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】マイクロポストを降順で得るテスト
/sample_app/test/models/micropost_test.rb
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end
【Fixture】マイクロポストのサンプルデータを定義する
/sample_app/test/fixtures/microposts.yml
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を指定する。

/sample_app/app/models/micropost.rb
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オプションを追加する。

/sample_app/app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy

【Test】Dependent: destroyのテスト

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

  • ユーザーをDBに保存する
  • ユーザーに紐付いたマイクロポストを生成する
  • マイクロポストの数が−1されていることを確認する
    • ユーザーを削除する
/sample_app/test/models/user_test.rb
  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は本当に便利ですね。

6
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
6
1