はじめに
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テーブルを作成する。
【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人のユーザーに所属する。
belongs_to :user
で関連付けを表すことができる。
class Micropost < ApplicationRecord belongs_to :user
【Model】ユーザーがマイクロポストを複数所有する(has_many)
User側から見ると、1人のユーザーが複数のMicropostを持つ。
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は本当に便利ですね。