6.1.1 データベースの移行
rails generate controller Users new
Userモデルを生成する
rails generate model User name:string email:string
Userモデルのマイグレーションdb/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
rails db:migrate
演習
1:Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。
これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために
使われます。さて、環境にあるdb/schema.rbの内容を調べ、その内容と
マイグレーションファイルの内容を比べてみてください。
schema.rb ActiveRecord::Schema.define(version: 20170203234505) 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
20200203234505_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
2:ほぼすべてのマイグレーションは、元に戻すことが可能です
元に戻すことを「ロールバック (rollback)と呼び、
Railsではdb:rollbackというコマンドで実現できます。
rails db:rollback上のコマンドを実行後、db/schema.rbの内容を調べてみて、
ロールバックが成功したかどうか確認してみてください。
Railsではdb:rollback
コマンドして、ロールバックの成功を確認
3:もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。
rails db:migrate
schema.rb ActiveRecord::Schema.define(version: 0) do end
User.createでモデルの生成と保存を同時におこなう方法も提供されています。
User.createは、trueかfalseを返す代わりに、ユーザーオブジェクト自身を返す
ことに注目してください。
返されたユーザーオブジェクトは (上の2つ目のコマンドにあるfooのように) 変数に代入することもできます。
演習6.1.2.1
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)
演習6.1.2.2
同様にして、ApplicationRecordがActiveRecord::Baseを継承していることについて確認してみてください。
>> ApplicationRecord.superclass => ActiveRecord::Base
演習
1:user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。
>> user.name.class => String >> user.email.class => String
2:created_atとupdated_atは、どのクラスのインスタンスでしょうか?
>> user.updated_at.class => ActiveSupport::TimeWithZone >> user.created_at.class => ActiveSupport::TimeWithZone
演習6.1.4.1
1:nameを使ってユーザーオブジェクトを検索してみてください。
また、find_by_nameメソッドが使えることも確認してみてください
User.find_by(name: "Michael Hartl") User.find_by_name(1)
2:実用的な目的のため、User.allはまるで配列のように扱うことができますが、
実際には配列ではありません。User.allで生成されるオブジェクトを調べ、
ArrayクラスではなくUser::ActiveRecord_Relationクラスであること
を確認してみてください。
User.all.class => User::ActiveRecord_Relation
3:User.allに対してlengthメソッドを呼び出すと、その長さを求められること
を確認してみてください。
Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかるという性質があります。
これをダックタイピング (duck typing) と呼び、よく次のような格言
で言い表されています「もしアヒルのような容姿で、アヒルのように鳴く
のであれば、それはもうアヒルだろう」
User.all.length User Load (0.2ms) SELECT "users".* FROM "users" => 2
保存を行わずにreloadを実行すると、データベースの情報を元にオブジェクトを再読み込みするので、次のように変更が取り消されます。
update_attributesメソッドは属性のハッシュを受け取り、
成功時には更新と保存を続けて同時に行います
特定の属性のみを更新したい場合は、次のようにupdate_attributeを使います。このupdate_attributeには、検証を回避するといった効果もあります
演習6.1.5.1
1:userオブジェクトへの代入を使ってname属性を使って更新し、
saveで保存してみてください。
user.name = 'suzu aki' user.save
2:今度はupdate_attributesを使って、email属性を更新および保存してみて
ください。
user.update_attribute(:email, "suzutuki@example.com")
3:同様にして、マジックカラムであるcreated_atも直接更新できることを確認してみてください。
ヒント: 更新するときは「1.year.ago」を使うと便利です。
これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
user.update_attribute(:created_at, 1.year.ago)
検証(Validation) 存在性(presence)の検証、長さ(length)の検証、
フォーマット(format)の検証、一意性(uniqueness)の検証です
よく使われる最終検証として確認 (confirmation)を追加します。
rails test:modelsというコマンドを実行していますが、
これはモデルに関するテストだけを走らせるコマンドです
演習
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> >> @user.valid? => true
2:この前生成したuserオブジェクトも有効であるかどうか、確認してみましょう。
User.new user.valid? => true
今回の場合、検証が1つしかないので、どの検証が失敗したかわかります。
しかし、失敗したときに作られるerrorsオブジェクトを使って確認すれば、
さらに便利です。
>> user.errors.full_messages => ["Name can't be blank"]
演習
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"]
2:u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。
emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
>> u.errors.messages => {:name=>["can't be blank"], :email=>["can't be blank"]} >> u.errors.messages[:email] => ["can't be blank"]
nameの長さの検証に対するテスト red test/models/user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end . . . test "name should not be too long" do @user.name = "a" * 51 assert_not @user.valid? end test "email should not be too long" do @user.email = "a" * 244 + "@example.com" assert_not @user.valid? end end
演習
1:長すぎるnameとemail属性を持ったuserオブジェクトを生成し、
有効でないことを確認してみましょう。
user = User.new(name: "a"*51, email: "b"*256) => #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...", created_at: nil, updated_at: nil> >> user.valid? => false
2:長さに関するバリデーションが失敗した時、どんなエラーメッセージが
生成されるでしょうか? 確認してみてください。
user.errors.full_messages => ["Name is too long (maximum is 50 characters)", "Email is too long (maximum is 255 characters)"]
有効なメールアドレスと無効なメールアドレスのコレクションに対するテスト
を行いましょう。このコレクションを作る方法として、文字列の配列を簡単に
作れる%w[]という便利なテクニックを知っておくと良いでしょう。
>> %w[foo bar baz] => ["foo", "bar", "baz"] >> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp] => ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"] >> addresses.each do |address| ?> puts address >> end USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp
メールアドレスのバリデーションは扱いが難しく、
エラーが発生しやすい部分なので、
有効なメールアドレスと無効なメールアドレスをいくつか用意して、
バリデーション内のエラーを検知していきます。
具体的には、user@example,comのような無効なメール
アドレスが弾かれることと、
user@example.comのような有効なメールアドレスが通ることを確認しながら、
バリデーションを実装していきます (ちなみに今の状態では、
空でない文字列はすべてメールアドレスとして通ってしまいます) 。
チュートリアルではもっと実用的で、堅牢であることが実戦で保証されている正規表現を採用します。これが、その正規表現です。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
この正規表現を理解するために、お手頃なサイズに分割してまとめたよ。
正規表現 意味
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i (完全な正規表現)
/ 正規表現の開始を示す
\A 文字列の先頭
[\w+\-.]+ 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d\-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
\. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わりを示す
i 大文字小文字を無視するオプション
正規表現VALID_EMAIL_REGEXは定数
演習
1:有効なメールアドレスのリストと、無効なメールアドレスのリストをRubular
のYour test string:に転記してみてその後、正規表現を
Your regular expression:に転記して、有効なメールアドレスのみがすべて
マッチし、無効なメールアドレスはすべてマッチしないことを確認して下さい。
Rubularで確かめる
2:foo@bar..comのようにドットが連続した無効なメールアドレスを
許容してしまいます。
まずは、このメールアドレスの無効なメールアドレスリストに追加し、
これによってテストが失敗することを確認してください。
次に、少し複雑な正規表現を使ってテストがパスすることを確認してください。
user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com") end (中略) 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 end
3:foo@bar.comをRubularのメールアドレスのリストに追加し、
の正規表現をRubularで使ってみてください。
有効なメールアドレスのみがすべてマッチし、
無効なメールアドレスはすべてマッチしないことを確認してみましょう。
Rubularでテストする
メールアドレスの一意性を強制するために (ユーザー名として使うために)、
validatesメソッドの:uniqueオプションを使います。ただしここで重大な警告
があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、
実際にレコードをデータベースに登録する必要があります。
そのため、まずは重複したメールアドレスからテストしていきます
@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。dupは、同じ属性を持つデータを複製するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。
add_index :users, :email, unique: true
usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っています。
データベースに保存される直前にすべての文字列を小文字に変換する
という対策を採ります。
例えば”Foo@ExAMPle.CoM”という文字列が渡されたら、
保存する直前に”foo@example.com”に変換してしまいます。
これを実装するためにActive Recordのコールバック (callback) メソッドを利用します。
オブジェクトが保存される時点で処理を実行したいので、before_save
というコールバックを使います。
app/models/user.rb
class User < ApplicationRecord
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }
end
演習
1:メールアドレスを小文字にするテストをリスト
test/models/user_test.rbに追加してみましょう。
ちなみに追加するテストコードでは、
データベースの値に合わせて更新するreloadメソッドと、
値が一致しているかどうか確認するassert_equalメソッドを使っています
テストがうまく動いているか確認するためにも、before_saveの行を
コメントアウトしてredになること,コメ解除してgreenになることを確認してみましょう。
#before_save { self.email = email.downcase! }
テストして確認
2:テストスイートの実行結果を確認しながら、before_saveコールバックを
email.downcase!に書き換えてみましょう。ヒント: メソッドの末尾に!を
付け足すことにより、email属性を直接変更できるようになります (リスト 6.34)。
before_save { self.email = email.downcase! }
セキュアなパスワードの実装は、has_secure_passwordという
Railsのメソッドを呼び出すだけでほとんど終わってしまいます。
class User < ApplicationRecord . . . has_secure_password end
password_digestカラム用の適切なマイグレーションを生成します。
マイグレーション名は自由に指定できますが、
次に示すように、末尾をto_usersにしておくことをオススメします。
こうしておくと、usersテーブルにカラムを
追加するマイグレーションがRailsによって自動的に作成されるからです。
add_password_digest_to_usersというマイグレーションファイルを生成
するためには、次のコマンドを実行します。
rails generate migration add_password_digest_to_users password_digest:string rails db:migrate
bcrypt
をGemfile
に追加するGemfile
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
bundle install
多重代入 (Multiple Assignment) を使っていることに注目してください。
@user.password = @user.password_confirmation = “a” * 5
passwordとpasswordconfirmationに対して同時に代入をしています
(このケースでは、文字列の乗算を利用して5文字の文字列を代入しています)。
演習
1:有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
>> user=User.new(name:"Taro Yamada",email:"yamada@mail.com",password:"di") => #<User id: nil, name: "Taro Yamada", email: "yamada@mail.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$akDmag5oWlwhewcg/7moLe1jbqdCBZM5Lttfj4laKMR..."> >> user.valid? User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "yamada@mail.com"], ["LIMIT", 1]] => false
2:失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
>> user.errors.full_messages => ["Password is too short (minimum is 6 characters)"] User.create(name: "suzutuki akina", email: "suzutuki@example.com", ?> password: "renath", password_confirmation: "renath")
演習
1:コンソールを一度再起動して (userオブジェクトを消去して)、このセクション
で作ったuserオブジェクトを検索してみてください。
user = User.find_by(email: "mhartl@example.com")
2:オブジェクトが検索できたら、名前を新しい文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね、なぜうまくいかなかったのでしょうか?
user.name = "Yamada" => "Yamada" >> user.save
name属性を変更し、.saveを実行すると、変更していない箇所も全て保存を要求される。
すなわち、パスワードをレコードに保存することを要求するため、エラーが生じる。
変更及び保存をname属性のみに限定する必要がある。
3:今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
user.update_attribute(:name, "Yamada") (0.1ms) begin transaction SQL (0.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Yamada"], ["updated_at", 2017-02-05 02:36:54 UTC], ["id", 1]] (10.0ms) commit transaction => true