Ruby on Rails チュートリアルで30歳までに人生を変える(第6章)
こんにちは。opiyoです。
今回は、第6章をやっていきます。
第6章はユーザーのモデルを作っていきます。
railsチュートリアル6章の学んだこと
$ rails db:rollback
$ rails c --sandbox Running via Spring preloader in process 3062 Loading development environment in sandbox (Rails 5.0.0.1) Any modifications you make will be rolled back on exit
- モデルだけのテスト実行
$ rails test:models 1/1: [============================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.03454s 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
- オブジェクトが有効かどうかは
valid?
メソッドを使う ハッシュ化とは元に戻せない不可逆なデータに処理する
!!でそのオブジェクトが対応する論理値オブジェクトに変換できる
モデルの作成方法
$ rails generate model User name:string email:string invoke active_record create db/migrate/20160523010738_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml
- コントローラー名は複数形
- モデル名は単数形
- マイグレーションファイル
- update_attributesメソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行います (保存に成功した場合はtrueを返します)。 ただし、検証に1つでも失敗すると、 update_attributesの呼び出しは失敗します。
- 特定の属性のみを更新したい場合は、次のようにupdate_attributeを使います。このupdate_attributeには、検証を回避するといった効果もあります。
> u.update_attribute(name: "El Duderino") # この書き方ではダメ ArgumentError: wrong number of arguments (given 1, expected 2) >> user.update_attribute(:name, "El Duderino") # カンマが区切ってキーと値で渡す => true >> user.name => "El Duderino"
railsチュートリアル6章の演習解説
6.1.1 演習 データベースの移行
6.1.1.1
<問題>Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
<回答>
#db/schema.rb ActiveRecord::Schema.define(version: 20170630074441) do create_table "users", force: :cascade do |t| t.string "name" t.string "email" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
# db/migrate/20170629005430_create_users.rb class CreateUsers < ActiveRecord::Migration[5.0] def change create_table :users do |t| t.string :name t.string :email t.timestamps end end end
6.1.1.2
<問題>ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。 $ rails db:rollback 上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。 上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。 これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。 詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。
<回答>
ActiveRecord::Schema.define(version: 0) do end
6.1.1.3
<問題>もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。
<回答>
ActiveRecord::Schema.define(version: 20170630074441) do create_table "users", force: :cascade do |t| t.string "name" t.string "email" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
6.1.2 演習 modelファイル
6.1.2.1
<問題>Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。
<回答>
> u = User.new > u.class.superclass => ApplicationRecord(abstract)
6.1.2.2
<問題>同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。
<回答>
> u.class.superclass.superclass => ActiveRecord::Base
6.1.3 演習 ユーザーオブジェクトを作成する
6.1.3.1
<問題>user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
<回答>
* user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil, password_digest: nil> irb(main):019:0> user.name.class => String irb(main):020:0> user.email.class => String
6.1.3.2
<問題>created_atとupdated_atは、どのクラスのインスタンスでしょうか?
<回答>
* user.created_at.class => NilClass > user.updated_at.class => NilClass
6.1.4 演習 ユーザーオブジェクトを検索する
6.1.4.1
<問題>nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。
<回答>
* User.find_by(name: "Michael Hartl") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-14 00:50:04", updated_at: "2017-08-14 00:50:04", password_digest: "$2a$10$vp5fiCtIdDwU/AdskPGeKeZ35UdjtkZbPWnV3i7g0Rq..."> irb(main):030:0> User.find_by_name("Michael Hartl") User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-14 00:50:04", updated_at: "2017-08-14 00:50:04", password_digest: "$2a$10$vp5fiCtIdDwU/AdskPGeKeZ35UdjtkZbPWnV3i7g0Rq...">
6.1.4.2
<問題>実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。 User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。
<回答>
* User.all.class => User::ActiveRecord_Relation
6.1.4.3
<問題>User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
<回答>
User.all.length User Load (0.5ms) SELECT "users".* FROM "users" => 2
6.1.5 演習 ユーザーオブジェクトを更新する
6.1.5.1
<問題>userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
<回答>
> User.first User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-14 00:50:04", updated_at: "2017-08-14 00:50:04"> irb(main):002:0> u = User.first User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-14 00:50:04", updated_at: "2017-08-14 00:50:04"> irb(main):003:0> u.name => "Michael Hartl" irb(main):004:0> u.name = "Haku" => "Haku" irb(main):005:0> u.save (0.1ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]] SQL (0.3ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Haku"], ["updated_at", 2017-08-14 01:06:07 UTC], ["id", 1]] (1.3ms) commit transaction => true
6.1.5.2
<問題>今度はupdate_attributesを使って、email属性を更新および保存してみてください。
<回答>
> u.update_attributes(email: "haku@example.com") (0.1ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "haku@example.com"], ["id", 1], ["LIMIT", 1]] SQL (0.3ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "haku@example.com"], ["updated_at", 2017-08-14 01:10:15 UTC], ["id", 1]] (1.6ms) commit transaction => true
6.1.5.3
<問題>同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
<回答>
* u.update_attribute(:created_at, 1.year.ago) (0.1ms) begin transaction SQL (0.3ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", 2016-08-14 01:14:57 UTC], ["updated_at", 2017-08-14 01:14:57 UTC], ["id", 1]] (1.6ms) commit transaction => true
6.2.1 演習 有効性を検証する
6.2.1.1
<問題>コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。
<回答>
* user = User.new(name: "Example User", email: "user@example.com") => #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil> irb(main):009:0> user.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "user@example.com"], ["LIMIT", 1]] => true
6.2.1.2
<問題>6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
<回答>
* user = User.new(name: "Michael Hartl", email: "mhartl@example.com") => #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com", created_at: nil, updated_at: nil> irb(main):013:0> user.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] => true
6.2.2 演習 存在性を検証する
6.2.2.1
<問題>新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
<回答>
* u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> irb(main):008:0> u.invalid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]] => true irb(main):009:0> u.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT ? [["LIMIT", 1]] => false irb(main):010:0> u.errors.full_messages => ["Name can't be blank", "Email can't be blank", "Email is invalid"]
6.2.2.2
<問題>u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
<回答>
> errors = u.errors.messages # `errors.messages`でハッシュになる => {:name=>["can't be blank"], :email=>["can't be blank", "is invalid"]} irb(main):023:0* errors[:email] => ["can't be blank", "is invalid"]
6.2.3 演習 長さを検証する
6.2.3.1
<問題>長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
<回答>
* u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> irb(main):046:0> u.name = "a" * 51 => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" irb(main):047:0> u.name.size => 51 irb(main):048:0> u.email = "a" * 255 + "@co.jp" => "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@co.jp" irb(main):049:0> u.email.size => 261 irb(main):050:0> u.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@co.jp"], ["LIMIT", 1]] => false irb(main):051:0> u.error u.error_on_ignored_order_or_limit u.errors irb(main):051:0> u.error u.error_on_ignored_order_or_limit u.errors
6.2.3.2
<問題>長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
<回答>
> u.errors.full_messages => ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
6.2.4 演習 フォーマットを検証する
6.2.4.1
<問題>リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
<回答>
6.2.4.2
<問題>先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
<回答>
# app/models/user.rb class User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } end
# test/models/user_test.rb test "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com foo@bar..com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end
6.2.4.3
<問題>foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
<回答>
6.2.5 演習 一意性を検証する
6.2.5.1
<問題> リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
<回答> 問題なし
# before_saveをコメントアウトすると大文字、小文字が合致せずエラーになる $ rails test Running via Spring preloader in process 5880 Started with run options --seed 39690 FAIL["test_email_addresses_should_be_saved_as_lower-case", UserTest, 1.0127649949990882] test_email_addresses_should_be_saved_as_lower-case#UserTest (1.01s) Expected: "foo@example.com" Actual: "Foo@ExAMPle.CoM"
6.2.5.2
<問題>テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
<回答>
before_save { self.email = email.downcase } ↓ before_save { email.downcase! }
- 気になること - self.email.downcase! じゃなくていいの? emailをはどう参照されるのだろうか。 selfがあれば、そのオブジェクト自身ってわかる気がするけどそれを勝手にやってくれるってことなのかな...
6.3.2 演習 ユーザーがセキュアなパスワードを持っている
6.3.2.1
<問題>この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。
<回答>
* u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil> irb(main):016:0> u.name = "Haku" => "Haku" irb(main):017:0> u.email = "haku@co.jp" => "haku@co.jp" irb(main):018:0> u.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "haku@co.jp"], ["LIMIT", 1]] => false
6.3.2.2
<問題> なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
<回答>
irb(main):019:0> u.errors.full_messages => ["Password can't be blank"]
6.3.3 演習 パスワードの最小文字数
6.3.3.1
<問題>有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
<回答>
* u = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil> irb(main):007:0> u.name = "Haku" => "Haku" irb(main):008:0> u.email = "haku@co.jp" => "haku@co.jp" irb(main):009:0> u.password = u.password_confirmation = "haku" => "haku" irb(main):010:0> u.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "haku@co.jp"], ["LIMIT", 1]] => false
6.3.3.2
<問題> 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
<回答>
> u.errors.full_messages => ["Password is too short (minimum is 6 characters)"]
6.3.4 演習 ユーザーの作成と認証
6.3.4.1
<問題>コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。
<回答>
$ rails c Running via Spring preloader in process 6789 Loading development environment (Rails 5.0.0.1) No entry for terminal type "ake/version"; using dumb terminal settings. irb(main):001:0> User.find_by(email: "mhartl@example.com") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-15 06:49:16", updated_at: "2017-08-15 06:49:16", password_digest: "$2a$10$ZDicArmstR/XhkV.IKJVu.hMWDn5oh.P3pilUSBlJOi...">
6.3.4.2
<問題>オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
<回答>
> user = User.find_by(email: "mhartl@example.com") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "mhartl@example.com"], ["LIMIT", 1]] => #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2017-08-15 06:49:16", updated_at: "2017-08-15 06:49:16", password_digest: "$2a$10$ZDicArmstR/XhkV.IKJVu.hMWDn5oh.P3pilUSBlJOi..."> irb(main):003:0> user.name = "Haku" => "Haku" irb(main):004:0> user.save (0.2ms) begin transaction User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ? [["email", "mhartl@example.com"], ["id", 1], ["LIMIT", 1]] (0.1ms) rollback transaction => false irb(main):005:0> user.errors.full_messages => ["Password can't be blank", "Password is too short (minimum is 6 characters)"] #
name
だけ変更してsave
しているが、password
も保存することを要求されるので「password
」が無いよってエラーになる。
name
だけ明示的に変更する場合は、6.3.4.3の方法を利用する。
6.3.4.3
<問題>今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
<回答>
* user.update_attribute(:name, "Haku") (0.1ms) begin transaction SQL (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Haku"], ["updated_at", 2017-08-15 06:59:37 UTC], ["id", 1]] (1.9ms) commit transaction => true irb(main):010:0> user.name => "Haku"