#6.1 Userモデル
##6.1.1 データベースの移行
###演習1
Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。
ActiveRecord::Schema.define(version: 20200420085033) 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
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps #db/schema.rbには無い記述?
end
end
end
ブロックの最後の行t.timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム (Magic Columns)」を作成します。
とあるので、同じようなものなのでしょうか?
###演習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: 20200420085033) 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
ActiveRecord::Schema.define(version: 0) do
end
ロールバックが成功しました!
###演習3
もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。
ActiveRecord::Schema.define(version: 20200420085033) 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ファイル
###演習1
Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください (ヒント: 4.4.4で紹介したテクニックを使ってみてください)。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> User.superclass
=> ApplicationRecord(abstract)
###演習2
同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。
>> User.superclass
=> ApplicationRecord(abstract)
>> User.superclass.superclass
=> ActiveRecord::Base
##6.1.3 ユーザーオブジェクトを作成する
###演習1
user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
>> user.name.class
=> String
>> user.email.class
=> String
###演習2
created_atとupdated_atは、どのクラスのインスタンスでしょうか?
>> user.created_at.class
=> ActiveSupport::TimeWithZone
>> user.updated_at.class
=> ActiveSupport::TimeWithZone
##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: "2020-04-26 05:40:26", updated_at: "2020-04-26 05:40:26">
>>
>> 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: "2020-04-26 05:40:26", updated_at: "2020-04-26 05:40:26">
###演習2
実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。
>> User.all.class
=> User::ActiveRecord_Relation
Arrayクラス:配列を表すオブジェクト
なぜ確認をしたのだろう。
###演習3
User.allに対してlengthメソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
>> User.all.length
User Load (0.2ms) SELECT "users".* FROM "users"
=> 2
Userテーブルには2個データが入っている、という意味だろうか。
##6.1.5 ユーザーオブジェクトを更新する
###演習1
userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
>> user.name = "Sheep"
=> "Sheep"
>> user.save
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Sheep"], ["updated_at", "2020-04-26 06:18:05.000602"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
###演習2
今度はupdate_attributesを使って、email属性を更新および保存してみてください。
>> user.update_attribute(:email, "sheep@email.com")
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "sheep@email.com"], ["updated_at", "2020-04-26 06:19:58.412542"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
###演習3
同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
>> 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", "2019-04-26 06:23:28.509787"], ["updated_at", "2020-04-26 06:20:52.052432"], ["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
##6.2.1 有効性を検証する
###演習1
コンソールから、新しく生成したuserオブジェクトが有効 (valid) であることを確認してみましょう。
>> user = User.new(name: "Tanukichi")
=> #<User id: nil, name: "Tanukichi", email: nil, created_at: nil, updated_at: nil>
>> user.valid?
=> true
###演習2
6.1.3で生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
>> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> 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>
>> user.valid?
=> true
##6.2.2 存在性を検証する
###演習1
新しいユーザーuを作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]
名前とEmailアドレスが入力されていないよ!
###演習2
u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
##6.2.3 長さを検証する
###演習1
長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
user = User.new(name:"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@example.com")
user.valid?
=> false
###演習2
長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
user.errors.full_messages
=> ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
##6.2.4 フォーマットを検証する
###演習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.2.5 一意性を検証する
###演習1
リスト 6.33のように、メールアドレスを小文字にするテストをリスト code:code-validates_uniqueness_of_email_case_insensitive_testに追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。
(確認だけなので省略)
###演習2
テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
(!を付け足すだけ?)
##6.3 セキュアなパスワードを追加する
###演習1
この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。
user = User.new(name: "coco", email: "coco@email.com")
=> #
user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "coco@email.com"], ["LIMIT", 1]]
=> false
###演習2
なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
user.errors.full_messages
=> ["Password can't be blank"]
passwordが設定されていないから。
##6.3.3 パスワードの最小文字数
###演習1
有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
user = User.new(name: "mame" , email: "mame@email.com", password: "mame")
=> #
user.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "mame@email.com"], ["LIMIT", 1]]
=> false
###演習2
上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]
passwordが短すぎるよ。最小6文字だよ。
##6.3.4 ユーザーの作成と認証
###演習1
コンソールを一度再起動して (userオブジェクトを消去して)、このセクションで作った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]]
=> #
###演習2
オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
user.name = "Mamekichi"
=> "Mamekichi"
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", 3], ["LIMIT", 1]]
(0.1ms) rollback transaction
=> false
###演習3
今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
user.update_attributes(name: "Mamekichi")
(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", 3], ["LIMIT", 1]]
(0.1ms) rollback transaction
=> false
user
=> #
update_attributesはfalseになっているけど、nameは更新されている?
DBを確認したら、やはり更新されていないようです……???