Ruby
Rails

Ruby on Rails Tutorial 6章

6.1.1

演習

Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.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 マイグレーション」を参照してください。

もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。

1

migrate後の 

db/development.sqlite3

SnapCrab_DB Browser for SQLite - CUsersadminryzenDownloadsdevelopment(5)sqlite3_2019-4-10_12-19-31_No-00.png

Usersテーブルにid name

Email

Created_at

Updateed_atがある

rollbackすると消える

演習

1. Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。

2. 同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。

貼り付け元 https://railstutorial.jp/chapters/modeling_users?version=5.1#code-users_migration


>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> User.superclass
=> ApplicationRecord(abstract)

>> ApplicationRecord.superclass
=> ActiveRecord::Base

演習

1. user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。

2. created_atとupdated_atは、どのクラスのインスタンスでしょうか?


>> user.name.class
=> String
>> user.email.class
=> String
>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone

演習

1. nameを使ってユーザーオブジェクトを検索してみてください。また、 find_by_nameメソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_byをよく見かけることでしょう)。

2. 実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。

3. User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)


>> 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: "2019-04-10 04:56:30", updated_at: "2019-04-10 04:56:30">
>> 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: "2019-04-10 04:56:30", updated_at: "2019-04-10 04:56:30">
>> User.all.class
=> User::ActiveRecord_Relation

>> User.all.length
User Load (0.2ms) SELECT "users".* FROM "users"
=> 2

演習

1. userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。

2. 今度はupdate_attributesを使って、email属性を更新および保存してみてください。

3. 同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。


>> user.name = "Michael"
=> "Michael"
>> user.save
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "updated_at" = ?, "name" = ? WHERE "users"."id" = ? [["updated_at", "2019-04-10 07:28:42.828990"], ["name", "Michael"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.update_attribute(:email, "mhartl@example.net")
(0.1ms) SAVEPOINT active_record_1
SQL (0.5ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "mhartl@example.net"], ["updated_at", "2019-04-10 07:29:23.519503"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> user.update_attribute(:created_at, 1.year.ago)
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", "2018-04-10 07:30:17.177732"], ["updated_at", "2019-04-10 07:30:17.178432"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true

演習

1. コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。

2. 6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。

>> @user = User.new

=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> @user.valid?
=> true
2:以前生成したuserオブジェクトも有効かどうか検証

>> User.find(1).valid?
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> true
>> User.find(2).valid?
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> true

演習

1. 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。

2. 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。

>> user = User.new

=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> user.name = "a" *51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> user.email = "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com"
>> user.valid?
=> false

>> user.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
>>

演習

1. リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

2. 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。

3. foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。

リスト 6.23: 2つの連続したドッ

SnapCrab_Rubular a Ruby regular expression editor - Google Chrome_2019-4-11_10-20-30_No-00.png

テスト確認

VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d-]+(.[a-z\d-]+)*.[a-z]+\z/i

実装でテストが通る>

SnapCrab_Rubular a Ruby regular expression editor - Google Chrome_2019-4-11_10-24-35_No-00.png

foo@bar..com

も有効にマッチ

演習

1. リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。

2. テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。

before_saveの行をコメントアウトするとエラー確認


ec2-user:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 10629
Started with run options --seed 23577

FAIL["test_email_addresses_should_be_saved_as_lower-case", UserTest, 0.3943505509996612]
test_email_addresses_should_be_saved_as_lower-case#UserTest (0.39s)
Expected: "foo@example.com"
Actual: "Foo@ExAMPle.CoM"
test/models/user_test.rb:48:in `block in <class:UserTest>'

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

Finished in 0.43841s
14 tests, 29 assertions, 1 failures, 0 errors, 0 skips

before_save { self.email = email.downcase! }を
before_save { email.downcase! }

にかえてもテストは通った

演習

1. この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。

2. なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。

6.3.3 パスワードの


>> user = User.new(name: "yamada",email: "yamada@email.com")
=> #<User id: nil, name: "yamada", email: "yamada@email.com", created_at: nil, updated_at: nil, password_digest: nil>

>> user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "yamada@email.com"], ["LIMIT", 1]]
=> false

演習

1. 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。

2. 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。


>> user= User.new(name:"yamada",email:"yamada@example.com",password:"1234")
=> #<User id: nil, name: "yamada", email: "yamada@example.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$XzOGHWI26ChNNC0sUU6A0.yGZFaoFf3g0OcJxmeGVdQ...">
>> user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "yamada@example.com"], ["LIMIT", 1]]
=> false
>> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]

ユーザー作成確認

)sqlite3_2019-4-10_12-19-313333.png

演習

1. コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作ったuserオブジェクトを検索してみてください。

2. オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?

3. 今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。


>> 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: "2019-04-11 07:54:02", updated_at: "2019-04-11 07:54:02", password_digest: "$2a$10$hO4Ic/m6emGj03egTvc/5.b1zZgS/Gzff3S7MH9srwr...">
>> user.name="newyamada"
=> "newyamada"
>> user.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]]
(0.1ms) rollback transaction
=> false
>> user.update_attribute(:name, "El Duderino")
(0.1ms) begin transaction
SQL (1.9ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "El Duderino"], ["updated_at", "2019-04-11 08:43:38.448201"], ["id", 1]]
(5.5ms) commit transaction
=> true