はじめに
最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。
このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る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テーブルを作成する。
【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は本当に便利ですね。