ユーザーのモデルを作成する
ユーザー用のデータモデルの作成とデータを保存する手段の確保について学んでいく
6.1 Userモデル
ユーザーが登録した情報を保存するためのデータ構造を作成する
railsではデータモデルとして扱うデフォルトのデータ構造のことをモデルと呼ぶ
データを永続化するデフォルトの解決策としてデータベースを使ってデータを長期間保存する
データベースとやりとりをするデフォルトのrailsライブラリはActive Recordと呼ばれる
Active Recordはデータオブジェクトの作成保存検索のためのメソッドを持つ
railsにはマイグレーションという機能がある
データの定義をrubyで記述できる
$ git switch -c modeling-users
6.1.1 データベースのマイグレーション
nameとemailの2つの属性を持つユーザーをモデリングするところから始める
emailは一意のユーザー名として使う
railsでユーザーをモデリングするときは属性を明示的に識別する必要がない
データを保存する際にデフォルトでリレーショナルデータベースを使う
リレーショナルデータベースは、データ行で構成されるテーブルからなり、各行はデータ属性のカラム(列)を持つ
図6.2 6.3 nameとemailのカラムを持つusersテーブルを作成する
モデルを作成するときはgenerate modelというコマンドを使う
コントローラ名は複数形を使い、モデル名には単数形を使う
リスト6.1 Userモデル作成
name:stringやemail:stringオプションのパラメータを渡すとDBで使いたい属性をrailsに伝える
属性の型情報も伝える
generateコマンドを実行するとマイグレーションファイルも生成されている。マイグレーションはDBの構造を段階的に変更する手段を提供する。要件が変更された場合にデータモデルを適合させる
リスト6.2 Userモデルのマイグレーションファイル
ファイル名の冒頭は生成された日時がタイムスタンプとして追加される
DBに与える変更を定義したchangeメソッドの集まり
↑の場合changeメソッドはcreate_tableメソッドを呼びユーザーを保存するためのテーブルをDBに作成する
create_tableメソッドはブロック変数を1つ持つブロックを受け取る。ここではtableの頭文字のt
ブロックの中でcreate_tableメソッドはtオブジェクトを使ってnameとemailカラムをDBに作る
モデル名は単数形テーブル名は複数形
モデルは1人のユーザーを表すのに対し、DBのテーブルは複数のユーザーから構成される
最後の行t.timestampsは特別なコマンドで、created_atとupdated_atの2つのマジックカラムを作成する
これらはあるユーザーが作成または更新されたときにその時刻を自動的に記録するタイムスタンプ
図6.4 マイグレーションによって作成されたデータモデル
マイグレーションの適用コマンド
$ rails db:migrate
6.1.2 モデルファイル
リスト6.3 Userモデル
class User < ApplicationRecordという構文でUserクラスはApplicationRecordを継承するので、Userモデルは自動的にActiveRecord::Baseクラスの全ての機能を持つことになる
6.1.3 ユーザーオブジェクトを作成する
railsコンソールを使ってデータモデルを調べてみる。今の時点でDBを変更したくないのでコンソールでサンドボックスモードで起動する
$ rails console --sandbox
サンドボックスで起動すると、そのセッションで行ったDBへの変更をコンソールの終了時にすべてロールバック(取り消し)する
User.newを引数なしで呼んだ場合は全ての属性がnilになっているオブジェクトを返す
user = User.new(name: "Michael Hartl", email: "michael@example.com")
user
#<User id: nil, name: "Michael Hartl", email: "michael@example.com",
created_at: nil, updated_at: nil>
nameとemail属性が設定される
ActiveRecordを理解する上で有効性(Validity)という概念が重要
valid?メソッドでオブジェクトが有効か確認できる
user.valid?
true
現時点でDBにデータは格納されていない。User.newは単にメモリ上でオブジェクトを作成しただけで、user.valid?もオブジェクトが有効か確認しただけでDBにデータが存在するかとは関係ない
DBにUserオブジェクトを保存するにはuserオブジェクトのsaveメソッドを呼び出す必要がある
user.save
saveメソッドを実行した後は、idに1が代入され、マジックカラムであるcreated_atとupdated_at属性の値に現在の日時が代入される
Userクラスと同様にUserモデルのインスタンスはドット記法を用いてその属性にアクセスできる
user.name
"Michael Hartl"
user.email
"michael@example.com"
user.updated_at
Fri, 11 Mar 2022 01:51:03 UTC +00:00
モデルの生成と保存を2つのステップに分けておくと何かと便利
しかし、生成と保存を同時に行うこともできる
User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2022-03-11 01:53:22", updated_at: "2022-03-11 01:53:22">
foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2022-03-11
01:54:03", updated_at: "2022-03-11 01:54:03">
User.createはtrueかfalseを返す代わりにユーザーオブジェクト自身を返す
destroyメソッドはcreateの逆
削除されたオブジェクトはメモリ上にまだ残っている
オブジェクトが本当に削除されたかどうかをどのように知れば良いのか
保存して削除されていないオブジェクトの場合どうやってDBからユーザーを取得するのか
Active Recordを使ってUserオブジェクトを検索する方法を見ていく
6.1.4 ユーザーオブジェクトを検索する
User.findにユーザーのidを渡すとそのidを持つユーザーを返す
User.find(1)
#<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
id=3のユーザーがいるか確認
User.find(3)
Couldn't find User with 'id'=3 (ActiveRecord::RecordNotFound)
6.1.3で3番目のユーザーを削除したのでDBから見つけることはできなかった
findメソッドは例外を発生する
属性を指定して検索する方法もある
User.find_by(email:"michael@example.com")
#<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
ユーザーをサイトにログインさせる方法を学ぶときに役立つ
User.first
#<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2022-03-11 01:51:03", updated_at: "2022-03-11 01:51:03">
firstはDBの最初のユーザーを返す
6.1.5 ユーザーオブジェクトを更新する
更新方法は2つある
1つは属性を個別に代入する
user.email = "mhartl@example.net"
"mhartl@example.net"
saveを行わずにreloadを実行するとDBの情報を元に再読み込みするので変更が取り消される
saveを実行するとマジックカラムの更新日時も更新されている
もう1つの更新方法はupdateを使う
user.update(name: "The Dude", email: "dude@abides.org")
true
user.name
"The Dude"
user.email
"dude@abides.org"
updateメソッドは属性の8種を受け取り成功時に更新と保存を同時に行う
特定の属性のみを更新したい場合はupdate_attributeを使う
user.update_attribute(:name, "El Duderino")
true
user.name
"El Duderino"
6.2 ユーザーを検証する
現状、name属性とemail属性に空文字などどんな値でも渡すことができてしまう
これらを避けるためにActive Recordで検証(Validation)という機能を使って制約を与える
よく使われるのは、存在性(presence)の検証、長さ(length)の検証、フォーマット(format)の検証、一意性(uniqueness)の検証
よく使われる最終検証として確認(confirmation)
6.2.1 有効性を検証する
バリデーション機能はテスト駆動開発との相性がいい
最初に失敗するするテストを書き、次にテストを成功させるように実装すればバリデーションできているか確認できる
まず有効なモデルのオブジェクトを作成し、その属性のうちの1つを有効でない属性に意図的に変更する
そして、バリデーションで失敗するかどうかテスト
念の為、最初に作成時の状態に対してもテストを書いておき、最初のモデルが有効かどうかも確認しておく
バリデーションのテストが失敗したときバリデーションの実装に問題があったのか、オブジェクトそのものに問題があったのか確認できる
リスト6.5 有効なUserかどうかをテストする
setupメソッドで有効なUserオブジェクト@userを作成。このメソッド内の処理は各テストが走る直前に実行される
@userはインスタンス変数だがsetupメソッド内で宣言しておけば全てのテスト内で使えるようになる
テストを実行した際にエラーが発生した
igrations are pending. To resolve this issue...
解消するためにマイグレーションを実行した
が、再びエラー
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:...
マイグレーションをリセットして再びマイグレーションすると解決した
6.2.2 存在性を検証する
基本的なバリデーション存在性Presence
渡された属性が存在するかどうか確認する
リスト6.7 以前のテストにname属性の存在性に関するテスト追加
@user変数のname属性に対して空白の文字列をセットして、assert_notメソッドでUserオブジェクトが有効でなくなったことを確認する
リスト6.9 name属性の存在を検査する方法は、validatesメソッドにpresence: true引数を与える。この引数は要素を1個持つオプションハッシュ
メソッドの最後の引数としてハッシュを渡す場合は{}をつけなくて良い
失敗したときに作られるerrorsオブジェクトを使って確認すると便利
user.errors.full_messages
["Name can't be blank"]
リスト6.11 6.12 emailにも同様のテストとvalidation追加
6.2.3 長さを検証する
webサイトに表示される名前の長さに制限を与える
リスト6.14 nameとemailの長さの検証に対するテスト
リスト6.16 name属性とemail属性に長さの検証追加
:lengthは長さの上限を強制する
6.2.4 フォーマットを検証する
emailのフォーマットに関する検証
リスト6.18 有効なメールフォーマットテスト
assertメソッドの第2引数にエラーメッセージを追加しているのでどのメールアドレスでテストが失敗したのか特定できる
リスト6.19 無効なアドレスを使って無効性(Invalidity)についてテスト
メールアドレスのフォーマットを検証するにはformatオプションを使う
validates :email, format: { with: // }
このオプションは引数に正規表現(Regular Expression)(regexとも)を取る
文字列のパターンマッチングにおいて強力
有効なメールアドレスだけにマッチして無効なメールアドレスにはマッチしない正規表現を組み立てる必要がある
VALID_EMAIL_REGEX = /\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i
/ 正規表現の開始
\A 文字列の先頭
[\w+-.]+ 英数字、アンダースコア_、プラス+、ハイフン-、ドット.のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わり
i 大文字小文字を無視するオプション
https://rubular.com/ 正規表現を試せるサイト
冒頭の\Aと\zを取り除くと複数のメールアドレスを1回で検知できるようになる
両端の/も取り除く
リスト6.21 メールフォーマットを正規表現で検証
大文字で始まる名前はRubyの定数 VALID_EMAIL_REGEX
6.2.5 一意性を検証する
ユーザー名として使うメールアドレスの一意性を強制するためにvalidatesメソッドの:uniquenessオプションを使う
これまでモデルのテストではUser.newを使ってきた。このメソッドは単にメモリ上にRubyのオブジェクトを作るだけ
しかし、一意性のテストのためには実際にレコードをDBに登録する必要がある
リスト6.24 重複するメールアドレスのテスト
duplicate 複製
@user.dupで同じ属性を持つデータを複製する
リスト6.25 emailバリデーション追加
リスト6.26
通常メールアドレスは大文字小文字が区別されない
メールアドレスの検証ではこのような場合も考慮する必要がある
大文字と小文字を区別しないでテストすることが必要
リスト6.27
メールアドレスの大文字小文字を無視した一意性の検証
case_sensitiveオプションfalseに置き換える
問題が一つ
ActiveRecordはDBのレベルでは一意性を保証していない
ユーザー登録時に登録ボタンを誤って素早く2回クリックするとリクエストが2件送信され同じメールアドレスを持つユーザーがDBに2つ保存されてしまう。
これを解決するにはDBレベルでも一意性を強制するだけで解決する
具体的にはDB上のemailのカラムにインデックスを追加し、そのインデックスが一意であるようにすればよい
コラム6.2
DBにカラムを作成するときそのカラムでレコードを検索する必要が生じるかどうか考える
例えばログインする際に送信されたメールアドレスと一致するユーザーのレコードをDBから探す必要がある
ユーザーをメールアドレスで検索するにはDBの全てのユーザーを1人ずつ探さなければならない(全表スキャン)
この問題はemailカラムにインデックスを追加することで解決する(本の索引のようなもの)
emailインデックスを追加するにはデータモデリングの変更が必要
マイグレーションでインデックスを追加する
すでに存在するモデルに構造を追加するのでmigrationジェネレーターを使ってマイグレーションを直接作成する
$ rails generate migration add_index_to_users_email
リスト6.29 マイグレーションファイル
メールアドレスの一意性を強制する
usersテーブルのemailカラムにインデックスを追加するためにadd_indexというメソッドを使う
unique: true オプションで一意性を強制する
マイグレーション実行
$ rails db:migrate
マイグレーションが失敗する場合にはサンドボックスのコンソールセッションを終了させる
動いているセッションがDBをロックしている可能性がある
この時点ではテストDB用のサンプルデータのメールアドレスの一意性がないのでテストはRED
リスト6.30 fixture内のサンプルデータはバリデーションで使われていなかったのでこれまでは問題にならなかった
リスト6.31 今のところは使わないので消しておく
メールアドレスの一意性を保証するにはもう一つ問題がある
一部のDBアダプタが大文字小文字を区別するインデックスを常に使っているとは限らない問題への対処
Foo@ExAMPle.Comとfoo@example.comを別々の文字列として解釈するDBがあるが、今回のアプリケーションではこれらの文字列は同一であると解釈されるべき
この問題を解決するため、DBに保存される直前に全ての文字列を小文字に変換する
ActiveRecordのコールバックメソッドで実装する。これはActiveRecordオブジェクトが存在する間の特定の時点で呼び出される
今回はオブジェクトが保存されるタイミングで処理を実行したいのでbefore_saveコールバックを使う
リスト6.32 ユーザーをDBに保存する前にemail属性を強制的に小文字に変換する
uniqueness制約をtrueに戻す
メールアドレスが小文字に統一されたので大文字小文字を区別する必要がなくなったため
右式のselfは省略できるが、左式のselfは省略できない
self.email = self.email.downcase
リスト6.33 テストを元に戻す
6.3 セキュアなパスワードを追加する
各ユーザーにパスワードとパスワードの確認を入力させ、ハッシュ化してDBに保存する
ハッシュ化とはハッシュ関数を用いて入力されたデータを不可逆(復元不可能な)データに変換すること
ユーザーの認証はパスワードの送信、ハッシュ化DB内のハッシュ化された値との比較の手順で進む
ハッシュ化された値同士を比較することで生のPWを保存せずに済む
6.3.1 ハッシュ化されたパスワード
セキュアなパスワードの実装はhas_secure_passwordメソッドを呼び出す。Userモデルで
このメソッドを使うと
セキュアにハッシュ化したPWをDB内のpassword_digest属性に保存できるようになる
2つの仮想的な属性(passwordとpassword_confirmation)が使えるようになる。存在性と値が一致するかどうかのバリデーションも追加される
authenticateメソッドが使えるようになる(引数の文字列がPWと一致するとUserオブジェクトを返し、一致しない場合はfalseを返す)
has_secure_password機能を使えるようにするには1つ条件がある
それはモデル内にpassword_digest属性が含まれていること
digestという言葉は暗号化用ハッシュ関数という用語が語源
ハッシュ化されたPWと暗号化されたPWは類義語
図6.9 Userモデルにpassword_digest属性追加
password_digestカラムを作成するための適切なマイグレーションを生成する
マイグレーション名の末尾をto_usersにしておく
railsが認識するとusersテーブルにカラムを追加するマイグレーションが自動的に作成される
$ rails generate migration add_password_digest_to_users password_digest:string
password_digest:string引数を与えて、今回必要になる属性名と型情報を渡している
リスト6.1でusersテーブルを最初に生成したときにname:stringやemail:string引数を与えたように、password_digest:string引数を与えると、railsに完全なマイグレーションを構築するための十分な情報を指定できる
リスト6.36 password_digestカラムを追加するマイグレーション
add_columnメソッドを使ってusersテーブルpassword_digestカラムを追加している
$ rails db:migrate
マイグレーション実行
has_secure_passwordでPWをハッシュ化するためには最先端のハッシュ関数のbcryptライブラリが必要
リスト 6.37 bcryptをgemfileに追加
6.3.2 ユーザーがセキュアなパスワードを持っている
リスト6.38
Userモデルにhas_secure_password追加
現時点ではテストが失敗する
理由はhas_secure_passwordには仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も追加されているから
リスト6.40 userテストにPWとPW確認追加
setup内の1行目の末尾にカンマを追加する
6.3.3 パスワードの最小文字数
リスト6.42 パスワードが空でないことや最小文字数をテストする
多重代入を使っている
@user.password = @user.password_confirmation = "a" * 5
PWとPW確認に対して同時に代入を行っている
リスト6.43
Userモデルにバリデーション追加
minimumオプションでPWの最小文字数を設定
空のPWを入力させないために存在性のバリデーション追加
has_secure_passwordメソッドには存在性のバリデーション機能も含まれているが、このバリデーションは空PWを持つレコードにしか適用されない
has_secure_passwordメソッドだけではユーザーが6文字分の空白といった無効なPWを作成可能になってしまう
6.3.4 ユーザーの作成と認証
DBに新規ユーザーを1人作成しておく
まだwebからユーザー登録はできないのでrailsコンソールを使って手動で作る
$ rails console
User.create(name: "Michael Hartl", email: "michael@example.com",password: "foobar", password_confirmation: "foobar")
password_digest属性を参照するとハッシュ化されていることがわかる
has_secure_passwordをUserモデルに追加したことでオブジェクト内でauthenticateメソッドが使える
これは引数のパスワードとDB内のハッシュ化されたPWが一致するか検証する
6.4 最後に
この章ではUserモデルを作成し、name属性、email属性、パスワード属性とバリデーションを追加した
コミット
$ rails test
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"
mainブランチにマージ、リモートリポジトリにプッシュ
$ git switch main
$ git merge modeling-users
$ git push
本番環境でUserモデルを使うにはRender上でもマイグレーションを実行する必要がある
$ touch bin/render-build.sh
設定ファイルを作る
リスト6.45 マイグレーションを実行するビルドコマンドを設定する
設定したらコミットしてGitHubにプッシュ
$ rails test
$ git add -A
$ git commit -m "Add build script"
$ git push
デプロイ時にrenderが設定ファイルを読み込めるようBuild Command欄にファイルのパス(./bin/render-build.sh)を設定
変更を保存したら手動でデプロイしてビルドコマンドが実行されたか確認する
ダッシュボードからログを確認する
6.4.1 本章のまとめ
・マイグレーションを使うと、アプリケのデータモデルを修正できる
・ActiveRecordを使うと、データモデルの作成や操作を行う多数のメソッドが使えるようになる
・ActiveRecordのバリデーションを使うと、モデルに対して制約を追加できる
・よくあるバリデーションには。存在性・長さ・フォーマットなどがある
・正規表現は一見謎めいているが強力
・DBにインデックスを追加すると検索の効率が向上。DBレベルでの一意性を保証するためにも使われる
・has_secure_passwordメソッドを使うと、モデルに対してセキュアなパスワードを追加できる