#第6章
この章はユーザーモデルを作っていく。前章はスタブページまでで終わったが、これからひたすらユーザー登録に関することを学んでいく。
(なんと本章から第12章までひたすら作っていくのでしんどい道となる)
頑張ろう
##Userモデル
ユーザー登録するからユーザー登録用ページを作る前に、まずは名前、メールアドレス、パスワード等を保存するためのデータ構造を作成しなければならない。
このデータ構造を、Railsでは**Model(モデル)**という。
また、データベースとやり取りするためのRailsライブラリは、Active Recordと呼ばれる。
通常データベースを使うなら、SQLという言語を使うが、Railsではマイグレーション機能で、データベースの構造を切り離してくれるため、SQLを意識しなくてもOK
(この辺りの機能便利だなーと思った。筆者は仕事でSQLを多少なりと触っていた経験があるので、SQLをいじらず作成できるのはでかい)
なおRailsチュートリアルでは、開発環境ではSQLiteを使用し、本番環境はPostgreSQLを使う。
###データベース移行
コンソールでいくつかデータを作成したが、これだけでは永続性が欠けている。ということで、永続性を保つためにモデルを作ることとする。
復習 命名規則
・コントローラー名は、複数形を使う。 例:Users
・モデル名は、単数形を使う。例:User
理由は、コントローラーは様々なデータを持つのに対し、モデルは決まった特定のデータだけを参照するため。
早速、モデルを作成する。
$ rails generate model User name:string email:string
name:string
やemail:string
オプションのパラメータを渡し、データベースで使う属性を与えている。
generate
コマンドを使うと、マイグレーションファイル、テストファイル、フィクスチャを同時に作ってくれる。
マイグレーションファイルを見てみよう。
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
タイムスタンプというものが発行されることが分かる。複数の開発者が同じ整数を持つマイグレーションを生成した際に、コンフリクトを起こさないようにするためのもの。
name
とemail
だけしか属性を与えていないが、他にもid
とcreated_at
とupdated_at
が作られている。
先ほどのt.timestampsによって
created_atと
updated_at`というマジックカラムが作成される。作成と更新日時を保持してくれる。
さて、ここまで来たらマイグレーションを適用させよう。
$ rails db:migrate
ついでに、DB Browser for SQLiteというソフトを使えばデータベースの構造を見れる。
演習
スキーマとマイグレーションファイルの比較
ActiveRecord::Schema.define(version: 2021_05_23_082848) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
class CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
2.ロールバック実行
$ rails db:rollback
ロールバックとはもとに戻すことを意味する。
$ rails db:rollback
-- drop_table(:users)
-> 0.0089s
スキーマの内容を見ると、、、
ActiveRecord::Schema.define(version: 0) do
end
drop_table
コマンドを呼び出し、削除している。change
メソッドがcreate_table
とdrop_table
のコマンドを知っている。
あとは、もう一度rails db:migrate
すればOK
###modelファイル
第4章で学んだ継承関係をモデルでもみる事ができる。
例えば、Userモデルは下のような感じだ。
class User < ApplicationRecord
end
class User < ApplicationRecord < ActiveRecord::Baseといった具合に継承している。
演習
2.6.3 :001 > User.new
(4.5ms) SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
User.new
でUser
クラスのオブジェクトが生成された。
###ユーザーオブジェクト生成
サンドボックスモードでコンソールを起動する。サンドボックスモードはコンソール終了時にすべてロールバックしてくれる。
$ rails console --sandbox
要点だけ
・User.new
を使うことで、新しいオブジェクトを生成できる。引数なしで読んだ場合は、全ての属性がnil
のオブジェクトとなる。
・user
オブジェクトが有効かどうかを調べるには、user.valid?
とすることで調べられる。
・保存するには、save
メソッドを使って、user.save
として、成功すれば保存する。
・Userモデルのインスタンスの属性にアクセスするには、ドット記法を用いる。
user.name
やuser.email
等
・モデルの生成と保存を一度に行える方法がある。それは、create
メソッドだ。User.create(name: "値", email: "値")
とすればOK
・create
の逆でオブジェクトを削除するには、destroy
メソッドを使うが、オブジェクトはメモリ上に残る。
演習
1.user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認
>> user.name.class
=> String
>> user.email.class
=> String
2.created_atとupdated_atは、どのクラスのインスタンスか。
>> foo.created_at.class
=> ActiveSupport::TimeWithZone
>> foo.updated_at.class
=> ActiveSupport::TimeWithZone
###ユーザーオブジェクトを検索する
Active Recordはオブジェクトを検索するための方法がいくつもある。
・1番目のユーザーを探す。User.find(id)
>> User.find(1)
番号を変えれば、その番号に該当するユーザーが取得できる。
・属性と検索値で探す。User.find_by(属性: "値")
>> User.find_by(email: "michael@example.com")
・最初のユーザーを検索する。User.first
データベースの最初のユーザーを返す。
>> User.first
・デーベースの全てのユーザーを返す。User.all
>> User.all
全てのオブジェクトのクラスがActiveRecord::Relation
となっており、各オブジェクトを配列としてまとめてくれている。
つまりはActiveRecord::Relation
クラスで、各オブジェクトを配列という扱いで返してくれるようになっている。
演習
>> User.all.class
=> User::ActiveRecord_Relation
>> User.all.length
###ユーザーオブジェクトを更新する。
ユーザーオブジェクトを更新する方法は2通りある。
1つ目
属性を個別に代入する方法
>> user.name = "ZIP"
=> "ZIP"
>> user.save
この方法を必ずsaveを実行する必要がある。saveを行わずreloadすると元のオブジェクトを再読み込みするので、変更が取り消される。
2つ目
update
を使う方法
>> user.update(name: "The Dude", email: "dude@abides.org")
update
メソッドは属性のハッシュを受け取り、成功した時は更新と保存を同時にしてくれる。しかし、検証に一つでも失敗すれば、update
の呼び出しは失敗する。
また、特定の属性のみ更新したい場合は、update_attribute
とすることで、検証を回避することができる。
>> user.update_attribute(:name, "El Duderino")
##ユーザーを検証する
name
とemail
属性が与えられたUserモデルを作ってきた。
しかし、現時点ではどんなデータも入力できてしまう。
name
なら、空白とかめっちゃ長い名前とか
email
なら、空白は当然ながら、メールアドレスのフォーマットに従ってないやつとか。
これらを解決しようってのが、**検証(Validation)**という訳。
検証でよく使われるやつ
・存在性(presence)
・長さ(length)
・フォーマット(format)
・一意性(uniqueness)
・確認(confirmation)
###有効性を検証する
検証が正しく動作しているかを進めるにあたり、テスト駆動開発で行っていく。
今までのように失敗→成功という順序。
テスト方法
①有効なモデルのオブジェクトを生成
②その属性のうち一つを有効でない属性に意図的に変更
③バリデーションで失敗するかどうかのテストをする
ユーザーモデルのテスト
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
end
setup
メソッドを使って、@user
を作成する。setupメソッド内に書かれた処理は、各テストが走る直前に実行される。
また、@user
はインスタンス変数だが、setupメソッド内で宣言したため、全てのテストでこのインスタンス変数が使えるようになる。
valid?
メソッドを使い、Userオブジェクトの有効性をテストできればOK。
$ rails test:models
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
モデルに関係するテストだけ実行している。まだUserモデルにバリデーションがないので、テストは成功している。
###存在性を検証する
基本と言っていい「存在性(Presence)」というバリデーション。
これは、渡された属性が存在するかどうかを検証するもの。
存在性のバリデーションがあれば、渡された属性にデータがあるということを保証できる。
name
属性の存在性テストを追加
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = " "
assert_not @user.valid?
end
end
テストする
$ rails test:models
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips
次にバリデーションを追加する。
class User < ApplicationRecord
validates :name, presence: true
end
このバリデーション記法は丸カッコと波カッコが省略されている。
省略せずにかくとこんな感じ
validates(:name, {presence: true})
validatesメソッドの第一引数に、ハッシュのキー:name
を渡し、第二引数にオプションハッシュを渡している。メソッドの丸カッコは省略できる&メソッド引数の最後のハッシュは省略できる。
コンソールでやるとこんな感じ
$ rails console --sandbox
>> user = User.new(name: "", email: "michael@example.com")
>> user.valid?
=> false
名前の属性が空白になっているため、存在性の検証でfalseが返ってきている。1つ以上失敗した時、falseを返す。
逆にすべてのバリデーションがOKならtrueを返す。
検証に失敗した時に作られるerrors
オブジェクトを使うと何で失敗したかが分かる。
>> user.errors.full_messages
=> ["Name can't be blank"]
当然保存も失敗する。
>> user.save
=> false
バリデーションを追加したのでテスト
$ rails test:models
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
同様にメールアドレスのバリデーションも追加
先ずはテストから
test "email should be present" do
@user.email = " "
assert_not @user.valid?
end
バリデーション追加
validates :email, presence: true
テスト
$ rails test
10 runs, 20 assertions, 0 failures, 0 errors, 0 skips
###長さを検証する
次の検証は長さ
あまりにも長い名前やメールアドレスは登録できないようにしたい。
ということで、長さの上限を設ける。
名前は50文字を上限
メールアドレスは、殆どのデータベースの文字列上限の255字としているので、255文字まで
name
とemail
の長さに対するテスト
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
指定の文字列を作成するには、文字列の掛け算をすればOK
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
テスト追加したので、テスト
$ rails test
12 runs, 22 assertions, 2 failures, 0 errors, 0 skips
長さの強制するための検証の引数
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
先ほど追加した存在性の後ろに長さの検証を追加した。
:maximum
パラメータと一緒に、:length
で長さの上限を決められる。
バリデーション追加したのでテスト
$ rails test
12 runs, 22 assertions, 0 failures, 0 errors, 0 skips
###フォーマットを検証する
name
属性は、空文字でない&51文字未満であればOK
しかし、email
属性は、それだけではなく有効なメールアドレスのフォーマットかどうか判定しなければならない。
有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを作る方法がある。
それは、文字列の配列を作れる%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
まずは、テストを追加
有効なメールフォーマットのテスト
test "email validation should accept valid addresses" do
valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
first.last@foo.jp alice+bob@baz.cn]
valid_addresses.each do |valid_address|
@user.email = valid_address
assert @user.valid?, "#{valid_address.inspect} should be valid"
end
end
追加したテストのassertメソッドの第2引数にエラーメッセージがある。
これは、どのメールアドレスが失敗したか特定できるようにしたもの。
文字列を調べるのに、inspect
メソッドを使用している。
また、each
メソッドを使って各メールアドレスを順にテストしている。
続いて、user@example,comやuser_at_foo.org等の無効なメールアドレスを使用し、「無効性(Invalidity)」のテストもする。
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]
invalid_addresses.each do |invalid_address|
@user.email = invalid_address
assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
end
end
テスト
$ rails test
14 runs, 28 assertions, 1 failures, 0 errors, 0 skips
メールアドレスのフォーマット検証には次のようなformat
というオプションを使う。
validates :email, format: { with: /<regular expression>/ }
このオプションの引数に正規表現を取る。その正規表現の一つが下のもの
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
モデルに検証を追加した結果が下のもの
class User < ApplicationRecord
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 }
end
因みに大文字で始まるものは、Rubyでは定数としている。
現時点では、foo@bar..com
の誤りは検出できない。
そのため、下記のように変更すればOK。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
バリデーションを追加したのでテスト
$ rails test:models
7 runs, 15 assertions, 0 failures, 0 errors, 0 skips
###一意性を検証する
一意性とは、他とダブらないということ。
同じものがあってはダメ。
一意性のテストの注意点
・メモリ上にオブジェクトを作るだけではなく、レコードをデータベースに登録する必要がある。
ユーザー1を登録して、ユーザー1と同じメールアドレスを持つユーザー2を登録するという状況を作る。
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
dup
メソッドは同じ属性を持つデータを複製するためのもの。
このままでは、メールアドレスの大文字と小文字が区別されないため、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じになる。
なので、テストコードにupcase
を下記に変更
test "email addresses should be unique" do
duplicate_user = @user.dup
duplicate_user.email = @user.email.upcase
@user.save
assert_not duplicate_user.valid?
end
メールアドレスの一意性の検証をするために、uniqueness
を追加
class User < ApplicationRecord
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: true
end
しかし、ここではfalseになるため、:uniqueness
に:case_sensitve
というオプションを使う。
class User < ApplicationRecord
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 }
end
こうすることで、Railsはtrue
と判断する。
しかし、まだ問題がある。
それは、Active Recordはデータベースのレベルまで一意性を保証してない。
現状の検証では、ユーザー登録する前に検証している。なので、トラフィックが多い時や素早く連続2回クリックするとリクエストが2つ連続で送信される。
そうすると、最初のリクエスト1は検証にパスするユーザーを作成し、リクエスト2も同様になる。結果的に、リクエスト1とリクエスト2が保存される。
解決策は、emailカラムにインデックスを追加し一意性が担保されればOK。
もしインデックスがないと?
インデックスがないと、渡されたemailアドレスと、データベースレコードを一人ずつ比較していかなければならない。このことを全表スキャンといって、ユーザーが多ければ多いほどよろしくない。
インデックスを追加すれば、図書館の索引と同じように目印を辿るので簡単に見つけられる。
今回既にマイグレーションが生成されているものに対して、モデル構造に新たにインデックスを追加するので、migration
ジェネレータを使い直接作成する。
$ rails generate migration add_index_to_users_email
一意性のマイグレーションが未定義なので、下記のように設定
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
end
end
users
テーブルのemail
カラムにadd_index
を使って追加した。
また、unique: true
で一意性を確保
マイグレートする
$ rails db:migrate
テストがREDになるためtest/fixtures/users.yml
の中身を消しておく。
Foo@ExAMPle.Comとfoo@example.comを同列の文字列と解釈させるため、データベースに保存する前に文字列をすべて小文字に変換する。
これを実現するために、**コールバック(callback)**メソッドを利用する。
保存する前に実行したいので、before_save
というコールバックを使う。
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: true
end
downcase
という文字列メソッドで小文字に変換している。
また、uniqueness:{case_sensitive: false}
をuniqueness: true
に戻してOK。理由は、全部小文字にするから。
テストも大文字変換していた箇所を削除した。
なおself.email = self.email.downcase
ともかけるが、右辺のself
は省略できるため、self.email = email.downcase
と書いてもOK。
また、破壊的メソッドを使ったこれもOK
before_save { email.downcase! }
""セキュアなパスワードを追加する
パスワードの値をそのままデータベースに保存するのではなく、ハッシュ化して保存する。
ハッシュ関数を使って、不可逆的なデータをする処理の事を指す。
ユーザー認証は
①パスワード送信
↓
②ハッシュ化
↓
③データベース内のハッシュ化された値と比較
という手順で行う。
生のパスワードは危険なので、、、
###ハッシュ化されたパスワード
Userモデルで、has_secure_password
メソッドを呼び出せばほぼ完了
・has_secure_passwordの機能
・password_digest
という属性が保存できる
・password
とpassword_confirmation
が使える。また、存在性と値が一致するかのバリデーションも追加される。
・authenticate
メソッドが使える。
まだモデル内にpassword_digest
という属性がない。従ってUserモデルにpassword_digest
のマイグレーション追加のコマンドを実行
$ rails generate migration add_password_digest_to_users password_digest:string
users
とすればusers
テーブルに変更を与えることを伝えられ、password_digest:string
という引数を与えることで、完全なマイグレーションを生成するための情報をRailsに与えられる。
マイグレーション実行
$ rails db:migrate
has_secure_password
を使ってパスワードをハッシュ化するために、最先端のハッシュ関数であるbcrypt
が必要になる。
Gemfileに
gem 'bcrypt', '3.1.13'
と打ち込んで、bundle install実行
Userモデルにpassword_digest
属性を追加、bcrypt
も追加したので準備OK
Userモデル内に
has_secure_password
と書く。
ここでテストすると失敗する。
理由は、has_secure_password
には、仮想的なpassword
属性とpassword_confirmation
属性に対するバリデーション機能も追加されているから。
テストのsetup内にpassword: "foobar", password_confirmation: "foobar"
を追加する。
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
これでテストは成功する。
演習
> user = User.new(name: "zippp", email: "zippp@foo.com")
(0.4ms) SELECT sqlite_version(*)
=> #<User id: nil, name: "zippp", email: "zippp@foo.com", created_at: nil, updated_at: nil, password_digest: nil>
> user.valid?
User Exists? (0.8ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "zippp@foo.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["Password can't be blank"]
###パスワードの最小文字数
パスワードの長さが6文字以上の検証をするテストを追加
test "password should be present (nonblank)" do
@user.password = @user.password_confirmation = " " * 6
assert_not @user.valid?
end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?
end
ここでは多重代入を使っている。
@user.password = @user.password_confirmation = "a" * 5
パスワードとパスワード確認に対して同時に代入している。
次に、バリデーション追加。maximum
の反対のminimum
というオプションを使えばOK
validates :password, presence: true, length: { minimum: 6 }
因みに、ここのコードにも存在性のバリデーションを入れないと、既にあるユーザーのパスワードを空白6文字で更新できる問題が発生するため存在性のバリデーションがある。
テストは成功した。
演習
> user = User.new(name: "zipp", email: "zipp@example.com", password: "foo")
(0.4ms) SELECT sqlite_version(*)
=> #<User id: nil, name: "zipp", email: "zipp@example.com", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
> user.valid?
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "zipp@example.com"], ["LIMIT", 1]]
=> false
> user.errors.full_messages
=> ["Password is too short (minimum is 6 characters)"]
###ユーザーの作成と認証
railsコンソールを使って、開発環境にユーザーを一人新規作成する。
$ rails console
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?> password: "foobar", password_confirmation: "foobar")
password_digest
属性を参照すると、、、
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-05-24 21:42:11", updated_at: "2021-05-24 21:42:11", password_digest: [FILTERED]>
2.6.3 :007 > user.password_digest
=> "$2a$12$0rJcCo8udpl672xQqpPw9.hAxiuTQPc01N2bWLl5utKJMCkN4T2Oq"
見事にパスワードがハッシュ化されている。
前回has_secure_password
をUserモデルに追加したので、そのオブジェクト内でauthenticate
メソッドが使えるようになった。
このメソッドは、引数の文字列をハッシュ化した値と、データベース内のpassword_digest
カラムの値を比較する。
間違ったパスワードだと、、、false
>> user.authenticate("foobaz")
false
正しいパスワードだとOK
> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com", created_at: "2021-05-24 21:42:11", updated_at: "2021-05-24 21:42:11", password_digest: [FILTERED]>
正しいパスワードを入れると、ユーザー情報を返す。
!!
を付けて!!user.authenticate("foobar")
とすれば、理論値で返してくれる。
#最後に
あとは、GithubとHerokuにあげればOK
ユーザーモデル、検証、パスワード等追加して何となくWEBアプリケーションらしく
なってきた。