#ユーザーのモデルを作成する
■第6章
第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わった。本章では、一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいく。
Railsでは、本番アプリケーションに適した認証システムを自分で構築するアプローチも選択できる。筆者曰く、自分自身で認証システムを構築した経験があれば仕組みを理解し、必要に応じて変更することがずっと容易になるらしい。
##6.1 Userモデル
ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造の作成。
Railsは、データベースの細部をほぼ完全に隠蔽し、切り離してくれる。
本書ではSQLiteを開発環境で使い、また、PostgreSQLを(Herokuでの)本番環境で使う。
###6.1.1 データベースの移行
この節での目的は、簡単に消えることのないユーザーのモデルに構築。
Railsはデータを保存する際にデフォルトでリレーショナルデータベースを使う。リレーショナルデータベースは、データ行で構成されるテーブルからなり、各行はデータ属性のカラム(列)を持ちます。
$ rails generate model User name:string email:string
今回はname
やemail
といった属性を付けたUserモデルを使いたいので、このコマンドを打つ。name:string
やemail:string
オプションのパラメータを渡すことによって、データベースで使いたい2つの属性をRailsに伝える。
*コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習を頭に入れておく
今回のコマンドの結果で、マイグレーションと呼ばれる新しいファイルが生成される。
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
end
マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加された。複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、衝突を起こしていた。現在のタイムスタンプによる方法であれば、基本衝突は避けられる。
マイグレーションはchange
メソッドの集まり。このメソッドはcreate_table
というRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成する。
t.timestamps
は特別なコマンドで、created_at
とupdated_at
という2つの「マジックカラム(Magic Columns)」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプ。
rails db:migrate
このコマンドでマイグレーションの実行。development.sqlite3
というファイルが生成される。これはSQLite5データベースの実体である。
DB Browser for SQLiteをダウンロードしてusers
テーブルを確認するとこんな感じに。
###6.1.2 modelファイル
ApplicationRecordはActiveRecord::Baseを継承している。
###6.1.3 ユーザーオブジェクトを作成する
データベースを変更したくない時は、コンソールをサンドボックスモードで起動。
$ rails console --sandbox
ここからはコンソールで作業。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
引数なしで呼んだ場合は、すべての属性がnil
のオブジェクトを返す。
userを作ってみる。
>> 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>
nameとemail属性がちゃんと設定された。
valid?
メソッドでuser
オブジェクトが有効か確認。
>> user.valid?
true
現時点ではデータベースに格納されていないので、save
メソッドを使って保存。
>> user.save
(0.1ms) SAVEPOINT active_record_1
SQL (7.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"], ["email", "mhartl@example.com"], ["created_at", "2022-02-12 10:41:35.029896"], ["updated_at", "2022-02-12 10:41:35.029896"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
idが更新され、マジックカラムには現在の日時が代入されている。
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2022-02-12 10:41:35", updated_at: "2022-02-12 10:41:35">
###6.1.4 ユーザーオブジェクトを検索する
データベースに保存されているかどうかを確認するために、User.find
を使う。
User.find(1)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2022-02-12 10:41:35", updated_at: "2022-02-12 10:41:35">
User.find
にユーザーのidを渡している。その結果、Active Recordはそのidのユーザーを返す。他にもいろんなメソッドがある。
###6.1.5 ユーザーオブジェクトを更新する
更新の方法は基本的に2つ。1つは属性を個別に代入する方法。
>> user # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
変更をデータベースに保存するために最後にsaveを実行する必要がある。
もう1つの方法はupdate_attributes
を使うケース。
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
このメソッドは、属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行う。
##6.2 ユーザーを検証する
name
とemail
にあらゆる文字列を許すのは避けるべきなので、これらの属性値に、何らかの制約を与える。検証 (Validation) という機能を通して、こういった制約を課すことができるようになっている。
###6.2.1 有効性を検証する
まず有効なモデルのオブジェクトを作成し、その属性のうちの1つを有効でない属性に意図的に変更します。そして、バリデーションで失敗するかどうかをテストする、といった方針で進めていく。
###6.2.2 存在性を検証する
最も基本的なバリデーションは「存在性 (Presence)」。
user_test.rb
にname
属性の存在性に関するテストを追記。
user.rb
に、空白が入らないようにバリデーションを追加する。
class User < ApplicationRecord
validates :name, presence: true
end
そうしたら、空白を弾くように。
$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false
user.errors.full_messages
で、なんでエラーになったのか見れる。
email
でも同様のことをやる。
###6.2.3 長さを検証する
これでUserモデル上に名前を持つことを強制されるようになった。名前の長さにも制限を与える。
名前は50文字まで、emailは255文字まで。
user_test.rb
に追記。
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
user.rb
長さのバリデーション追加。
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
###6.2.4 フォーマットを検証する
有効なメールアドレスかどうかの判定をする。
メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい。
user_test.rb
で有効なメールフォーマットをテストする。
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 @user.valid?, "#{valid_address.inspect} should be valid"
次は無効性についてテスト。フォーマット検証には、format
を使う。
validates :email, format: { with: /<regular expression>/ }
メールフォーマットを正規表現で検証する。
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
正規表現VALID_EMAIL_REGEX
は定数。大文字で始まる名前はRubyでは定数を意味する。残る制約は、メールアドレスが一意であることを強制することだけとなった。
###6.2.5 一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validates
メソッドの:unique
オプションを使う。
一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要がある。
user_test.rb
に重複するメールアドレス拒否のテスト。
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
@user
と同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしている。
大文字小文字を区別しない一意性のテストも必要。
duplicate_user.email = @user.email.upcase
このコードを上のコードに追加。
ここまでやったことは、あくまでもアプリケーションレベルで一意性を強制しただけであるため、素早く2回連続でクリックしたなどのデータベースレベルでは一意性を強制したことにならない。
emailインデックスを追加すると、データモデリングの変更が必要になる。
今回は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーターを使ってマイグレーションを直接作成する必要がある。
$ rails generate migration add_index_to_users_email
メールアドレスの一意性のマイグレーションは未定義なので、[timestamp]_add_index_to_users_email.rb
に次のように定義。
class AddIndexToUsersEmail < ActiveRecord::Migration[5.1]
def change
add_index :users, :email, unique: true
end
end
users
テーブルのemail
カラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っている。
最後に、データベースをマイグレート。
$ rails db:migrate
データベースによって、メールアドレスの大文字と小文字を区別せずにバリデーションが効かないという問題がある。
これを、user.rb
で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: { case_sensitive: false }
end
##6.3 セキュアなパスワードを追加する
最後の砦である「セキュアなパスワード」に取り掛かる。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それをハッシュ化したものをデータベースに保存する。
今回の「ハッシュ化」は、ハッシュ関数を使って、入力されたデータを不可逆なデータにする処理を指す。
ハッシュ化することで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれる。
###6.3.1 ハッシュ化されたパスワード
セキュアなパスワードの実装は、has_secure_password
というRailsのメソッドを呼び出すだけでほぼ終わるっぽい。
class User < ApplicationRecord
.
.
.
has_secure_password
end
このメソッドを追加すると、次のような機能が使えるようになる。
・セキュアにハッシュ化したパスワードを、データベース内のpassword_digest
という属性に保存できるようになる。
・2つのペアの仮想的な属性 (password
とpassword_confirmation
) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
・authenticate
メソッドが使えるようになる (引数の文字列がパスワードと一致するとUser
オブジェクトを、間違っているとfalse
を返すメソッド) 。
has_secure_password
機能を使えるようにするには、モデル内にpassword_digest
という属性が含まれていることが条件となる。
今回はUserモデルにそれを追加する。そのために以下のコマンドを実行。
$ rails generate migration add_password_digest_to_users password_digest:string
上のコマンドではpassword_digest:string
という引数を与えて、今回必要になる属性名と型情報を渡している。
また、has_secure_password
を使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcrypt
が必要になる。bcrypt
gemをGemfile
に追加。
###6.3.2 ユーザーがセキュアなパスワードを持っている
$ rails test
を実行してもREDに。
セットアップメソッドでいろいろと情報が足りてないらしい。
user_test.rb
にパスワードとパスワード確認を追加。
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
これでテストはGREENに。
###6.3.3 パスワードの最小文字数
今回は簡潔にパスワードが空でないことと最小文字数 (6文字) の2つを設定する。
パスワードの長さが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
また、空のパスワードを入力させないために、存在性のバリデーションも追加。
###6.3.4 ユーザーの作成と認証
以上でUserモデルの基本部分が完了。次章でユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを1人作成しておく。
今度はrails consoleでサンドボックスでないパターン。
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?> password: "foobar", password_confirmation: "foobar")
うまくデータベースに保存されていました。
感想
この章から難易度がまた一段と上がった気がします。たまにエラーも出ましたが無事解決できました。
半年前くらいにインターンを始めようとしたときに「バックエンドとフロントエンド、どっちに興味があるの?」と聞かれた時、何もわかんなかったのが、この章で、バックエンドの人ってこんな感じのことやってんだろうなーとなんとなくですがわかった気がします。
疑問・単語集
*rails test
を実行しようとしたら
Migrations are pending. To resolve this issue, run:
bin/rails db:migrate RAILS_ENV=test
とエラーになってしまいました。以下のコードを実行したらうまくいきました。
$RAILS_ENV=test rails db:drop
$RAILS_ENV=test rails db:create
$RAILS_ENV=test rails db:migrate