Ruby on Rails チュートリアル
-
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
|
||
第3版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第3版 目次
- 第1章 ゼロからデプロイまで
- 第2章 Toyアプリケーション
- 第3章 ほぼ静的なページの作成
- 第4章 Rails風味のRuby
- 第5章 レイアウトを作成する
- 第6章 ユーザーのモデルを作成する
- 第7章 ユーザー登録
- 第8章 ログイン、ログアウト
- 第9章 ユーザーの更新・表示・削除
- 第10章 アカウント有効化とパスワード再設定
- 第11章 ユーザーのマイクロポスト
- 第12章 ユーザーをフォローする
第6章 ユーザーのモデルを作成する
第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わりました (5.4)。これから5つの章を通して、ユーザー登録ページを作っていくことにしましょう。本章では、一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいきます。第7章では、ユーザーがサイトにユーザー登録できるようにし、ユーザープロファイルのためのページを作成します。ユーザー登録できるようになったら、ログインやログアウトをできる仕組みを作り (第8章)、第9章からは不正なアクセスを取り扱う方法について学んでいきます (9.2.1) 。最後に、第10章でメールアドレスを使ってアカウントを有効化する方法と、パスワードをリセットする方法について学びます。まとめると、第6章から第10章を通して、Railsのログインと認証システムをひととおり開発します。ご存知の方もいると思いますが、Railsでは既にさまざまな認証方法が利用可能です。コラム6.1では、最初に少なくとも一度は自分で認証システムを作ってみることをお勧めする理由について説明しています。
事実上、すべてのWebアプリケーションは何らかのログイン/認証システムを必要とします。そのため、多くのWebフレームワークではこのようなログイン/認証システムを実装するための選択肢が多数提供されています。Railsもまた例外ではありません。認証 (authentication) と認可 (authorization) のシステムの例だと、Clearance、Authlogic、Devise、CanCanなどがあります (Railsに限らなければOpenIDやOAuthの上に構築する方法もあります)。なぜ車輪の再発明をするのか、という質問があるのも当然です。自分でわざわざ作らなくても、いつも使える方法をただ利用するだけではいけないのでしょうか。
ある実践的な実験によると、多くのサイトの認証システムは膨大なカスタマイズを必要とするため、サードパーティ製品を変更して導入する場合にはシステムをゼロから作成するよりも多くの仕事を要するという結果が出ています。加えて、既成品のシステムは内部がわかりづらいことが多く、ブラックボックスになっています。自分で作成したシステムであれば、それをとてもよく理解しているはずです。さらに言えば、最近のRailsへの変更 (6.3) により、カスタム認証システムを容易に作成できるようになりました。最後に、あえて最終的にサードパーティの認証システムを導入することになったとしても、自分自身で認証システムを構築した経験があれば、サードパーティ製品を理解して変更することがずっと容易になるはずです。
6.1 Userモデル
ここから3つの章にわたる最終目標はユーザー登録ページ (図6.1のモックアップ) を作成することですが、今のままでは新しいユーザーの情報を受け取っても保存する場所がないので、いきなりページを作成するわけにはいきません。ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造を作成することです。
Railsでは、データモデルで使用するデフォルトのデータ構造のことをモデルと呼びます (1.3.3で言うMVCのMのことです)。Railsでは、データを永続化するデフォルトの解決策として、データベースを使用してデータを長期間保存します。また、データベースとやりとりするデフォルトのRailsライブラリはActive Recordと呼ばれます1。Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っています。これらのメソッドを使用するのに、リレーショナルデータベースで使うSQL (Structured Query Language)2を意識する必要はありません。さらに、Railsにはマイグレーションという機能があります。データの定義をRubyで記述することができ、SQLのDDL (Data Definition Language)を新たに学ぶ必要がありません。Railsは、データストアの詳細からほぼ完全に私たちを切り離してくれます。本書では、SQLiteを開発 (development) 環境で使い、またPostgreSQLを (Herokuでの) 本番環境で使います (1.5)。Railsは、本番 (production) アプリケーションですら、データの保存方法の詳細についてほとんど考える必要がないくらいよくできています。
Gitでバージョン管理を行なっているのであれば、このタイミングでユーザーをモデリングするためのトピックブランチを作成しておいてください。
$ git checkout master
$ git checkout -b modeling-users
6.1.1 データベースの移行
4.4.5で扱ったカスタムビルドクラスのUser
を思い出してください。このクラスは、name
とemail
を属性に持つユーザーオブジェクトでした。このクラスは役に立つ例として提供されましたが、Railsにとって極めて重要な部分である永続性という要素が欠けていました。RailsコンソールでUserクラスのオブジェクトを作っても、コンソールからexitするとそのオブジェクトはすぐに消えてしまいました。この節での目的は、簡単に消えることのないユーザーのモデルを構築することです。
4.4.5のユーザークラスと同様に、name
とemail
の2つの属性からなるユーザーをモデリングするところから始めましょう。後者のemailを一意のユーザー名として使用します3 (パスワードのための属性は6.3で扱います)。リスト4.13では、以下のようにRubyのattr_accessor
メソッドを使用しました。
class User
attr_accessor :name, :email
.
.
.
end
それとは対照的に、Railsでユーザーをモデリングするときは、属性を明示的に識別する必要がありません。上で簡潔に述べたように、Railsはデータを保存する際にデフォルトでリレーショナルデータベースを使用します。リレーショナルデータベースは、データ行で構成されるテーブルからなり、各行はデータ属性のカラム (列) を持ちます。たとえば、nameとemailを持つユーザーを保存するのであれば、name
とemail
のカラムを持つusers
テーブルを作成します (各行は1人のユーザーを表します)。テーブルに格納されるデータの例を図6.2に、対応するデータモデルを図6.3に示します (なお、図6.3は草案です。実際のデータモデルは図6.4のようになります)。name
やemail
といったカラム名を今のうちに考えておくことで、後ほどUserオブジェクトの各属性をActiveRecordに伝えるときに楽になります。
リスト5.28で、ユーザーコントローラ (とnew
アクション) を作ったときに使った以下のコマンドを思い出してみてください。
$ rails generate controller Users new
モデルを作成するときは、上と似たようなパターンでgenerate model
というコマンドを使います。さらに、今回はname
やemail
といった属性を付けたUserモデルを使いたいので、実際に打つコマンドはリスト6.1になります。
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/20140724010738_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
(コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習を頭に入れておいてください。コントローラはUsersでモデルはUserです)。name:string
やemail:string
オプションのパラメータを渡すことによって、データベースで使用したい2つの属性をRailsに伝えます。このときに、これらの属性の型情報も一緒に渡します (この場合はstring
)。リスト3.4やリスト5.28でアクション名を使用して生成した例と比較してみてください。
リスト6.1にあるgenerate
コマンドの結果のひとつとして、マイグレーションと呼ばれる新しいファイルが生成されます。マイグレーションは、データベースの構造をインクリメンタルに変更する手段を提供します。それにより、要求が変更された場合にデータモデルを適合させることができます。このUserモデルの例の場合、マイグレーションはモデル生成スクリプトによって自動的に作られました。リスト6.2に示したようにname
とemail
の2つのカラムを持つusers
テーブルを作成します (6.2.5で、マイグレーションを一から手動で作成する方法について説明します)。
users
テーブルを作るための) Userモデルのマイグレーション db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps null: false
end
end
end
マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加されます。以前はインクリメンタルな整数が追加されましたが、複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、コンフリクトを引き起こしていました。現在のタイムスタンプによる方法であれば、まったく同時にマイグレーションが生成されるという通常ではありえないことが起きない限り、そのようなコンフリクトは避けられます。
マイグレーション自体は、データベースに与える変更を定義したchange
メソッドの集まりです。リスト6.2の場合、change
メソッドはcreate_table
というRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成します。create_table
メソッドはブロック変数を1つ持つブロック (4.3.2) を受け取ります。ここでは (“table”の頭文字を取って) t
です。そのブロックの中でcreate_table
メソッドはt
オブジェクトを使って、name
とemail
カラムをデータベースに作ります。型はどちらもstring
です4。モデル名は単数形 (User) ですが、テーブル名は複数形 (users
) です。これはRailsで用いられる言葉の慣習を反映しています。モデルはひとりのユーザーを表すのに対し、データベースのテーブルは複数のユーザーから構成されます。ブロックの最後の行t.timestamps
は特別なコマンドで、created_at
とupdated_at
という2つの「マジックカラム」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプです (このマジックカラムの使用例を6.1.3から具体的に見ていきます)。リスト6.2のマイグレーションによって作成された完全なデータモデルを図6.4に示します (図6.3のスケッチには無かったマジックカラムが追加されています)。
マイグレーションは、以下のようにrake
コマンド (コラム2.1) を使って実行することができます。これを「マイグレーションの適用 (migrating up)」と呼びます。
$ bundle exec rake db:migrate
(2.2で、このコマンドを似たような状況で実行したことを思い出してみてください) 。初めてdb:migrate
が実行されると、db/development.sqlite3
という名前のファイルが生成されます。これはSQLite5データベースです。db/development.sqlite3
ファイルを開くためのDB Browser for SQLiteという素晴らしいツールを使うと、データベースの構造を見ることができます (Cloud IDEを使っている場合は、図6.5のようにまずはファイルをお手元にダウンロードする必要があります)。結果は図6.6のようになるので、図6.4と比べてみてください。図6.6の中にid
というマイグレーションのときに説明されなかったカラムの存在に気づいたかもしれません。2.2で簡単に説明したとおり、このカラムは自動的に作成され、Railsが各行を一意に識別するために使用します。
Railsチュートリアルで使用されているものすべてを含め、ほとんどのマイグレーションが可逆です。これは、db:rollback
というRakeタスクで変更を取り消せることを意味します。これを“マイグレーションの取り消し (migrate down) と呼びます。
$ bundle exec rake db:rollback
(コラム3.1では、マイグレーションを元に戻すための便利なテクニックを他にも紹介しています)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_table
コマンドを内部で呼び出しています。これがうまくいくのは、change
メソッドはdrop_table
がcreate_table
の逆であることを知っているからです。つまり、ロールバック用の逆方向マイグレーションを簡単に導くことができるのです。あるカラムを削除するような不可逆なマイグレーションの場合は、change
メソッドの代わりに、up
とdown
のメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。
もし今の時点でデータベースのロールバックを実行していた場合は、先に進む前にもう一度以下のようにマイグレーションを適用して元に戻してください。
$ bundle exec rake db:migrate
6.1.2 modelファイル
これまで、リスト6.1のUserモデルの作成によってどのように (リスト6.2の) マイグレーションファイルが作成されるかを見てきました。そして図6.3でこのマイグレーションを実行した結果を見ました。users
テーブルを作成することで、development.sqlite3
という名のファイルを更新し、id
、name
、email
、created_at
、updated_at
を作成しました。また、リスト6.1ではモデル用のuser.rbも作られました。この節では、以後このモデル用ファイルを理解することに専念します。
まずは、app/models/
ディレクトリにある、user.rb
ファイルに書かれたUserモデルのコードを見てみましょう。これは控えめに言ってもとてもよくまとまっています (リスト6.3)
class User < ActiveRecord::Base
end
4.4.2で行ったことを思い出してみましょう。class User < ActiveRecord::Base
という構文で、User
クラスはActiveRecord::Base
を継承するので、Userモデルは自動的にActiveRecord::Base
クラスのすべての機能を持ちます。もちろん、継承されていることが分かっても、ActiveRecord::Base
に含まれるメソッドなどについて知らなければ何の役にも立ちません。それらの知識の一部についてこれから説明します。
6.1.3 ユーザーオブジェクトを作成する
第4章と同じく、Railsコンソールを使用してデータモデルを調べてみましょう。現時点ではデータベースを変更したくないので、コンソールをサンドボックスモードで起動します。
$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>
"Any modifications you make will be rolled back on exit" (ここで行ったすべての変更は終了時にロールバックされます) というメッセージにわかりやすく示されているように、コンソールをサンドボックスで起動すると、そのセッションで行ったデータベースへの変更をコンソールの終了時にすべて “ロールバック” (取り消し) してくれます。
4.4.5のコンソールセッションではUser.new
で新しいユーザーオブジェクトを生成しましたが、リスト4.13のexample_userファイルを明示的にrequireするまでこのオブジェクトにはアクセスできませんでした。しかし、モデルを使うと状況は異なります。4.4.4で見たように、Railsコンソールは起動時にRailsの環境を自動的に読み込み、その環境にはモデルも含まれます。つまり、新しいユーザーオブジェクトを作成するときに余分な作業を行わずに済むということです。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
上の出力は、ユーザーオブジェクトをコンソール用に出力したものです。
User.new
を引数なしで呼んだ場合は、すべての属性がnil
のオブジェクトを返します。4.4.5では、オブジェクトの属性を設定するための初期化ハッシュ (hash) を引数に取るように、Userクラスの例 (user_example.rb) を設計しました。この設計は、同様の方法でオブジェクトを初期化するActive Recordの設計に基づいています。
>> 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属性が期待どおり設定されていることがわかります。
また、Active Recordを理解する上で、「有効性 (Validity)」という概念も重要です。6.2で詳細について解説しますが、今はまず先ほどのuser
オブジェクトが有効かどうか確認してみましょう。確認するためにはvalid?
メソッドを使います。
>> user.valid?
true
現時点ではまだデータベースにデータは格納されていません。つまり、User.new
はメモリ上でオブジェクトを作成しただけで、user.valid?
という行はただオブジェクトが有効かどうかを確認しただけとなります (データベースにデータがあるかどうかは有効性には関係ありません)。データベースにUserオブジェクトを保存するためには、user
オブジェクトからsave
メソッドを呼び出す必要があります。
>> user.save
(0.2ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users".
"email") = LOWER('mhartl@example.com') LIMIT 1
SQL (0.5ms) INSERT INTO "users" ("created_at", "email", "name", "updated_at)
VALUES (?, ?, ?, ?) [["created_at", "2014-09-11 14:32:14.199519"],
["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
"2014-09-11 14:32:14.199519"]]
(0.9ms) commit transaction
=> true
save
メソッドは、成功すればtrue
を、失敗すればfalse
を返します (現状では、保存はすべて成功するはずです。失敗する場合については6.2で説明します)。また、Railsコンソール上ではuser.save
に対応するSQLコマンドやその結果 (INSERT INTO "users"
...) も表示するようになっています。なお、本書では生のSQLが必要になる場面がほとんどないので6、SQLコマンドに関する解説は省略します。とはいえ、Active Recordに対応するSQLコマンドをザッと眺めておくだけでも勉強にはなるはずです。
作成した時点でのユーザーオブジェクトは、id
属性、マジックカラムであるcreated_at
属性とupdated_at
属性の値がいずれもnil
であったことを思い出してください。save
メソッドを実行した後に何が変更されたのかを確認してみましょう。
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
id
には1
という値が代入され、一方でマジックカラムには現在の日時が代入されているのがわかります7。現在、作成と更新のタイムスタンプは同一ですが、6.1.5では異なる値になります。
4.4.5のUserクラスと同様に、Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます。
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Thu, 24 Jul 2014 00:57:46 UTC +00:00
詳細は第7章でも説明しますが、上で見たようにモデルの生成と保存を2つのステップに分けておくと何かと便利です。しかし、Active RecordではUser.create
でモデルの生成と保存を同時におこなう方法も提供されています。
>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2014-07-24 01:05:24", updated_at: "2014-07-24 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
User.create
は、true
かfalse
を返す代わりに、ユーザーオブジェクト自身を返すことに注目してください。返されたユーザーオブジェクトは (上の2つ目のコマンドにあるfoo
のように) 変数に代入することもできます。
destroy
はcreate
の逆です。
>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
create
と同じように、destroy
はそのオブジェクト自身を返しますが、その返り値を使用しても、もう一度destroy
を呼ぶことはできません。さらに、削除されたオブジェクトは、以下のようにまだメモリ上に残っています。
>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">
では、オブジェクトが本当に削除されたかどうかをどのようにして知ればよいのでしょうか。そして、保存して削除されていないオブジェクトの場合、どうやってデータベースからユーザーを取得するのでしょうか。これらの問いに答えるためには、Active Recordを使ってUserオブジェクトを検索する方法について学ぶ必要があります。
6.1.4 ユーザーオブジェクトを検索する
Active Recordには、オブジェクトを検索するための方法がいくつもあります。これらの機能を使用して、過去に作成した最初のユーザーを探してみましょう。また、3番目のユーザー (foo
) が削除されていることを確認しましょう。まずは存在するユーザーから探してみましょう。
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
ここでは、User.find
にユーザーのidを渡しています。その結果、Active Recordはそのidのユーザーを返します。
次に、id
=3
のユーザーがまだデータベースに存在するかどうかを確認してみましょう。
>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3
6.1.3で3番目のユーザーを削除したので、Active Recordはこのユーザーをデータベースの中から見つけることができませんでした。代わりに、find
メソッドは例外 (exception) を発生します。例外はプログラムの実行時に何か例外的なイベントが発生したことを示すために使われます。この場合、存在しないActive Recordのidによって、find
でActiveRecord::RecordNotFound
例外8が発生しました。
一般的なfind
メソッド以外に、Active Recordには特定の属性でユーザーを検索する方法もあります。
>> User.find_by(email: "mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
これまでメールアドレスをユーザー名として使用してきたので、このようなfind
関連メソッドは、ユーザーをサイトにログインさせる方法を学ぶときに役に立ちます (第7章)。ユーザー数が膨大になるとfind_by
では検索効率が低下するのではないかと心配する方もいるかもしれませんが、あせる必要はありません。この問題およびデータベースのインデックスを使った解決策については6.2.5で扱います。
ユーザーを検索する一般的な方法をあと少しだけご紹介して、この節を終わりにすることにしましょう。まず初めにfirst
メソッドです。
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
読んで字のごとく、first
は単にデータベースの最初のユーザーを返します。次はall
メソッドです。
>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2014-07-24 00:57:46",
updated_at: "2014-07-24 00:57:46">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2014-07-24 01:05:24",
updated_at: "2014-07-24 01:05:24">]>
コンソールの出力結果を見ると、User.all
でデータベースのすべてのUserオブジェクトが返ってくることがわかります。また、返ってきたオブジェクトのクラスが ActiveRecord::Relation
となっています。これは、各オブジェクトを配列として効率的にまとめてくれるクラスです(4.3.1)。
6.1.5 ユーザーオブジェクトを更新する
いったんオブジェクトを作成すれば、今度は何度でも更新したくなるものです。基本的な更新の方法は2つです。ひとつは、4.4.5でやったように属性を個別に代入する方法です。
>> user # Just a reminder about our user's attributes
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
変更をデータベースに保存するために最後にsaveを実行する必要があることを忘れないでください。保存を行わずにreload
を実行すると、データベースの情報を元にオブジェクトを再読み込みするので、以下のように変更が取り消されます。
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
user.save
を実行したことでユーザーが更新できました。6.1.3で約束したように、マジックカラムの更新日時も更新されています。
>> user.created_at
=> "2014-07-24 00:57:46"
>> user.updated_at
=> "2014-07-24 01:37:32"
属性を更新するもうひとつの方法は、update_attributes
を使うものです9。
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
update_attributes
メソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行います (保存に成功した場合はtrue
を返します)。ただし、検証に1つでも失敗すると、update_attributes
の呼び出しは失敗します。たとえば、6.3で実装する、パスワードをレコードに保存することを要求すると検証は失敗します。特定の属性のみを更新したい場合は、以下のようにupdate_attribute
を使います。なお、update_attributeには検証を回避するといった効果もあります。
>> user.update_attribute(:name, "The Dude")
=> true
>> user.name
=> "The Dude"
6.2 ユーザーを検証する
ついに、6.1で作成したUserモデルに、アクセス可能なname
とemail
属性が与えられました。しかし、これらの属性はどんな値でも取ることができてしまいます。現在は (空文字を含む) あらゆる文字列が有効です。名前とメールアドレスには、もう少し何らかの制限があってよいはずです。たとえば、name
は空であってはならず、email
はメールアドレスのフォーマットに従う必要があります。さらに、メールアドレスをユーザーがログインするときの一意のユーザー名として使おうとしているので、メールアドレスがデータベース内で重複することのないようにする必要もあります。
要するに、name
とemail
にあらゆる文字列を許すのは避けるべきです。これらの属性値には、何らかの制約を与える必要があります。Active Record では検証 (Validation) という機能を通して、こういった制約を課すことができるようになっています (実は2.3.2で少しだけ使っていました)。ここでは、よく使われるケースのうちのいくつかについて説明します。それらは存在性 (presence)の検証、長さ (length)の検証、フォーマット (format)の検証、一意性 (uniqueness)の検証です。6.3.2では、よく使われる最終検証として確認 (confirmation)を追加します。7.3では、ユーザーが制約に違反したときに、検証機能によって自動的に表示される有用なエラーメッセージをお見せします。
6.2.1 有効性のテスト
コラム3.3で言及したとおり、テスト駆動開発は仕事で常に正しく適用できるとは限りませんが、モデルのバリデーション機能は、テスト駆動開発とまさにピッタシの機能と言えます。バリデーション機能は強力ですが、うまく動いている自信を持つのが難しいです。しかし、(テスト駆動開発のように) まず失敗するテストを書き、次にテストを成功させるように実装すると、期待した通りに動いている自信を持てるようになります。
具体的なテスト方法についてですが、まず有効なモデルのオブジェクトを作成し、その属性のうちの1つを有効でない属性に意図的に変更します。そして、バリデーションで失敗するかどうかをテストする、といった方針で進めていきます。念のため、最初に作成時の状態に対してもテストを書いておき、最初のモデルが有効であるかどうかも確認しておきます。このようにテストすることで、バリデーションのテストが失敗したとき、バリデーションの実装に問題があったのか、オブジェクトそのものに問題があったのかを確認することができます。
リスト6.1のコマンドを実行してUser用テストの原型ができているはずなので、まずはその中身から見ていきましょう (リスト6.4)。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
有効なオブジェクトに対してテストを書くために、setup
という特殊なメソッドを使って有効なUserオブジェクト (@user
) を作成します (このメソッドは第3章の演習でも少し取り上げました)。setupメソッド内に書かれた処理は、各テストが走る直前に実行されます。@user
はインスタンス変数ですが、setupメソッド内で宣言しておけば、すべてのテスト内でこのインスタンス変数が使えるようになります。したがって、valid?
メソッドを使ってUserオブジェクトの有効性をテストすることができます (6.1.3)。これをコードにすると、リスト6.5のようになります。
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
リスト6.5では、シンプルなassert
メソッドを使ってテストします。@user.valid?
がtrue
を返すと成功し、false
を返すと失敗します。
とはいえ、Userモデルにはまだバリデーションがないので、このテストは成功するはずです。
$ bundle exec rake test:models
上ではrake test:models
というコマンドを実行していますが、これはモデルに関するテストだけを走らせるコマンドです (5.3.4で使ったrake test:integration
と似ていることに注目してください)。
6.2.2 存在性を検証する
おそらく最も基本的なバリデーションは「存在性 (Presence)」です。これは単に、与えられた属性が存在することを検証します。たとえばこの節では、ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証します。7.3.3では、この要求を新しいユーザーを作るためのユーザー登録フォームにまで徹底させる方法を確認します。
まずはリスト6.5に、name
属性の存在性に関するテストを追加します。具体的にはリスト6.7のように、まず@user
変数のname
属性に対して空白の文字列をセットします。そして、assert_not
メソッドを使って Userオブジェクトが有効でなくなったことを確認します。
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 "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = " "
assert_not @user.valid?
end
end
この時点では、モデルのテストは失敗するはずです。
$ bundle exec rake test:models
第2章の演習で少し触れましたが、name属性の存在を検査する方法は、リスト6.9に示したとおり、validates
メソッドにpresence: true
という引数を与えて使うことです。presence: true
という引数は、要素がひとつのオプションハッシュです。4.3.4のようにメソッドの最後の引数としてハッシュを渡す場合、波括弧を付けなくても問題ありません(5.1.1でも説明したように、Railsのオプションハッシュは繰り返し登場するテーマです)。
name
属性の存在性を検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true
end
リスト6.9は一見魔法のように見えるかもしれませんが、validates
は単なるメソッドです。括弧を使用してリスト6.9を同等のコードに書き換えたものを以下に示します。
class User < ActiveRecord::Base
validates(:name, presence: true)
end
コンソールを起動して、Userモデルに検証を追加した効果を見てみましょう10。
$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false
このように、user
変数が有効かどうかをvalid?
メソッドでチェックすることができます。もしオブジェクトがひとつ以上の検証に失敗したときは、false
を返します。 また、すべてのバリデーションに通ったときにtrue
を返します。今回の場合、検証が1つしかないので、どの検証が失敗したかわかります。しかし、失敗したときに作られるerrors
オブジェクトを使って確認すれば、さらに便利です。
>> user.errors.full_messages
=> ["Name can't be blank"]
(Railsが属性の存在性を検査するときに、エラーメッセージはヒントになります。これにはblank?
メソッドを用います。4.4.3の終わりに見ました)。
Userオブジェクトは有効ではなくなったので、データベースに保存しようとすると自動的に失敗するはずです。
>> user.save
=> false
この変更によりリスト6.7のテストは成功しているはずです。
$ bundle exec rake test:models
リスト6.7のモデルに倣って、email
属性の存在性についてもテストを書いてみましょう (リスト6.11)。最初は失敗しますが、リスト6.12のコードを追加することで成功するようになります。
email
属性の検証に対するテスト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 "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = ""
assert_not @user.valid?
end
test "email should be present" do
@user.email = " "
assert_not @user.valid?
end
end
email
属性の存在性を検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true
end
これですべての存在性がチェックされたので、テストスイートは成功するはずです。
$ bundle exec rake test
6.2.3 長さを検証する
各ユーザーは、Userモデル上に名前を持つことを強制されるようになりました。しかし、これだけでは十分ではありません。ユーザーの名前はサンプルWebサイトに表示されるものなので、名前の長さにも制限を与える必要があります。6.2.2で既に同じような作業を行ったので、この実装は簡単です。
最長のユーザー名の長さに科学的な根拠はありませんので、単に50
を上限として手頃な値を使うことにします。つまりここでは、51
文字の名前は長すぎることを検証します。また、実際に問題になることはほとんどありませんが、問題になる可能性もあるので長すぎるメールアドレスに対してもバリデーションを掛けましょう。ほとんどのデータベースでは文字列の上限を255としているので、それに合わせて255文字を上限とします。6.2.4で説明するメールアドレスのフォーマットに関するバリデーションでは、こういった長さの検証はできないので、本節で長さに関するバリデーションを事前に追加しておきます。結果をリスト6.14に示します。
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
リスト6.14では、51文字の文字列を簡単に作るために “文字列のかけ算” を使いました。結果をコンソール上で確認できます。
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
メールアドレスの長さに対するバリデーションも、次のように長い文字列を作成して検証します。
>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaa@example.com"
>> ("a" * 244 + "@example.com").length
=> 256
この時点では、リスト6.14のテストは失敗しているはずです。
$ bundle exec rake test
これをパスさせるためには、長さを強制するための検証の引数を使う必要があります。:maximum
パラメータと共に用いられる:length
は、長さの上限を強制します (リスト6.16)。
name
属性に長さの検証を追加する GREEN app/models/user.rb
class User < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
これでテストがGREENになるはずです。
$ bundle exec rake test
成功したテストスイートを流用して、今度は少し難しい、メールアドレスのフォーマット検証作業に取りかかりましょう。
6.2.4 フォーマットを検証する
name
属性の検証には、空文字でない、名前が51文字未満であるという最小限の制約しか与えていませんでした。email
属性の場合は、有効なメールアドレスかどうかを判定するために、もっと厳重な要求を満たさなければなりません。これまでは空のメールアドレスのみを禁止してきましたが、ここではメールアドレスにおなじみのパターンuser@example.com
に合っているかどうかも確認することを要求します。
なお、ここで使用するテストや検証は、形式がひとまず有効なメールアドレスを受け入れ、形式があからさまに無効なものを拒否するだけであり、すべての場合を網羅したものではないという点に注意してください。最初に、有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを行いましょう。このコレクションを作るために、以下のコンソールセッションに示したような、文字列の配列を簡単に作れる%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
each
メソッドを使ってaddresses
配列の各要素を繰り返し取り出しました (4.3.2)。このテクニックを学んだことで、基本となるメールアドレスフォーマット検証のテストを書く準備が整いました。
メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい部分なので、有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検知していきます。具体的には、user@example,comのような無効なメールアドレスが弾かれることと、user@example.comのような有効なメールアドレスが通ることを確認しながら、バリデーションを実装していきます (ちなみに今の状態では、空でないメールアドレスであれば全て通ってしまいます) 。まずは、有効なメールアドレスをリスト6.18に示します。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
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
end
ここでは、assertメソッドの第2引数にエラーメッセージを追加していることに注目してください。これによって、どのメールアドレスでテストが失敗したのかを特定できるようになります。
assert @user.valid?, "#{valid_address.inspect} should be valid"
(詳細な文字列を調べるために4.3.3で紹介したinspect
メソッドを使っています。) どのメールアドレスで失敗したのかを知ることは非常に便利です。そこでリスト6.18では、each
メソッドを使って各メールアドレスを順にテストしています。ループさせずにテストすると、失敗した行番号からとメールアドレスの行数を照らし合わせて、失敗したメールアドレスを特定するといった作業が発生してしまいます。
次に、 user@example,com (ドットではなくカンマになっている) やuser_at_foo.org (アットマーク ‘@’ がない) といった無効なメールアドレスを使って 「無効性 (Invalidity)」についてテストしていきます。リスト6.18と同様に、リスト6.19でもエラーメッセージをカスタマイズして、どのメールアドレスで失敗したのかすぐに特定できるようにしておきます。
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]
invalid_addresses.each do |invalid_address|
@user.email = invalid_address
assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
end
end
end
この時点では、テストは失敗するはずです。
$ bundle exec rake test
メールアドレスのフォーマットを検証するためには、次のようにformat
というオプションを使います。
validates :email, format: { with: /<regular expression>/ }
このオプションは引数に正規表現 (Regular Expression) (regexとも呼ばれます) を取ります。正規表現は一見謎めいて見えますが、文字列のパターンマッチングにおいては非常に強力な言語です。つまり、有効なメールアドレスだけにマッチして、無効なメールアドレスにはマッチしない正規表現を組み立てる必要があります。
メールアドレス標準を定める公式サイトに完全な正規表現があるのですが、非常に巨大かつ意味不明で、場合によっては逆効果になりかねるので11、本チュートリアルではもっと実用的で、堅牢であることが実戦で保証されている正規表現を採用します。 これが、その正規表現です。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
この正規表現を理解するために、お手頃なサイズに分割して表6.1にまとめました12。
表現 | 意味 |
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i | (完全な正規表現) |
/ | 正規表現の開始を示す |
\A | 文字列の先頭 |
[\w+\-.]+ | 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す |
@ | アットマーク |
[a-z\d\-.]+ | 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す |
\. | ドット |
[a-z]+ | 英小文字を少なくとも1文字以上繰り返す |
\z | 文字列の末尾 |
/ | 正規表現の終わりを示す |
i | 大文字小文字を無視するオプション |
表6.1からも多くのことを学べるとは思いますが、正規表現を本当に理解するためには実際に使って見るのが一番です。たとえばRubularという対話的に正規表現を試せるWebサイトがあります (図6.7)13。このWebサイトはインタラクティブ性に富んだインターフェイスを持っていて、また、正規表現のクイックリファレンスも兼ね備えています。Rubularをブラウザで開き、表6.1の内容を実際に試してみることを強くお勧めします。正規表現は、読んで学ぶより対話的に学んだほうが早いです。(注: 表6.1の正規表現をRubularで使う場合、冒頭の\Aと末尾の\zの文字は含めないでください)。
表6.1の正規表現を適用してemail
のフォーマットを検証した結果を、リスト6.21に示します。
class User < ActiveRecord::Base
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では定数を意味します。以下のコードは、
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
このパターンに一致するメールアドレスだけが有効であることをチェックします。上の正規表現には少しだけ残念な点があります。foo@bar..com
のようなドットの連続を誤りとして検出できません。この問題の修正するにはとてつもなく複雑な正規表現を使う必要がありますが、これは演習問題に回します (6.5)。
この時点で、テストは成功しているはずです。
$ bundle exec rake test:models
残る制約は、メールアドレスが一意であることを強制するだけとなりました。
6.2.5 一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validates
メソッドの:unique
オプションを使います。ただしここで重大な警告があります。以下の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。モデルのテストではこれまで、主にUser.new
を使ってきました。このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14。そのため、まずは重複したメールアドレスからテストしていきます (リスト6.23)。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
end
上のコードでは、@user
と同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。dupは、同じ属性を持つデータを複製するためのメソッドです。@user
を保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。
リスト6.23のテストをパスさせるために、email
のバリデーションにuniqueness: true
というオプションを追加します リスト6.24。
class User < ActiveRecord::Base
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
実装の途中ですが、ここでひとつ補足します。通常、メールアドレスでは大文字小文字が区別されません。すなわち、foo@bar.com
はFOO@BAR.COM
やFoO@BAr.coM
と書いても扱いは同じです。従って、メールアドレスの検証ではこのような場合も考慮する必要があります15 。このため、大文字を区別しないでテストすることが肝要になり、実際のコードはリスト 6.25のようになります。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
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
end
上のコードではStringのupcase
メソッドを使っています (4.3.2)。このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
$ rails console --sandbox
>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、duplicate_user.valid?
はtrue
になります。しかし、ここではfalse
になる必要があります。幸い、:uniquenessでは
)。:case_sensitive
という打ってつけのオプションが使用できます (リスト6.26
class User < ActiveRecord::Base
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.24のtrue
をcase_sensitive: false
に置き換えただけであることに注目してください (リスト6.26)。Railsはこの場合、:uniqueness
をtrue
と判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。
$ bundle exec rake test
しかし、依然としてここには1つの問題が残っています。それはActive Recordはデータベースのレベルでは一意性を保証していないという問題です。具体的なシナリオを使ってその問題を説明します。
- アリスはサンプルアプリケーションにユーザー登録します。メールアドレスはalice@wonderland.comです。
- アリスは誤って “Submit” を素早く2回クリックしてしまいます。そのためリクエストが2つ連続で送信されます。
- 次のようなことが順に発生します。リクエスト1は、検証にパスするユーザーをメモリー上に作成します。リクエスト2でも同じことが起きます。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存されます。
- この結果、一意性の検証が行われているにもかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。
上のシナリオが信じがたいもののように思えるかもしれませんが、どうか信じてください。RailsのWebサイトでは、トラフィックが多いときにこのような問題が発生する可能性があるのです (筆者もこれを理解するのに苦労しました)。幸い、解決策の実装は簡単です。実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。具体的にはデータベース上のemailのカラムにインデックス (index)を追加し (コラム6.2)、そのインデックスが一意であるようにすれば解決します。
データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要です。たとえば、リスト6.2のマイグレーションによって作成されたemail属性について考えてみましょう。第7章ではユーザーをサンプルアプリにログインできるようにしますが、このとき、送信されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探しだす必要があります。残念なことに、(インデックスなどの機能を持たない) 素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と与えられたメールアドレスを比較するという非効率的な方法しかありません。つまり、たとえばデータベース上の最後のユーザを探す場合でも、すべてのユーザーを最初から順に一人ずつ探していくことになります。これは、データベースの世界では全表スキャンとして知られており、数千のユーザーがいる実際のサイトでは極めて不都合です。
emailカラムにインデックスを追加することで、この問題を解決することができます。データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。索引のない本では、与えられた言葉 (例えば、“foobar”) が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります (紙バージョンの全表スキャン)。しかし索引のある本であれば、“foobar”を含むすべてのページを索引の中から探すだけで済みます。データベースのインデックスも本質的には本の索引と同じように動作します。
emailインデックスを追加すると、データモデリングの変更が必要になります。Railsでは (6.1.1で見たように) マイグレーションでインデックスを追加します。6.1.1で、Userモデルを生成すると自動的に新しいマイグレーションが作成されたことを思い出してください (リスト6.2)。今回の場合は、既に存在するモデルに構造を追加するので、以下のようにmigration
ジェネレーターを使用してマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。リスト6.28のように定義を記述する必要があります16。
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end
上のコードでは、users
テーブルのemail
カラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っています。インデックス自体は一意性を強制しませんが、オプションでunique: true
を指定することで強制できるようになります。
最後に、データベースをマイグレートします。
$ bundle exec rake db:migrate
(上のコマンドが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了してみてください。そのセッションがデータベースをロックしてマイグレーションを妨げている可能性があります)。
この時点では、(テストDB用のサンプルデータが含まれている) fixtures内で一意性の制限が保たれていないため、テストは失敗します。リスト6.1でユーザー用のfixtureが自動的に生成されていますが、メールアドレスが一意になっていません (リスト6.29)。(このデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなりませんでした。)
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう (リスト6.30)。
# empty
これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。それは、「いくつかのデータベースのアダプタは大文字小文字を区別するインデックスを使っている」という問題への対処です。例えば“Foo@ExAMPle.Com”と“foo@example.com”が別々の文字列だと解釈してしまうデータベースがありますが、私達のアプリケーションではこれらの文字列は同一であると解釈されるべきです。この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採ります。例えば“Foo@ExAMPle.CoM”という文字列が与えられたら、保存する直前に“foo@example.com”に変換してしまいます。これを実装するためにActive Recordのコールバック (callback) メソッドを利用します。このメソッドは、ある特定の時点で呼び出されるメソッドです。今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_save
というコールバックを使います。これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します17。これをコードにすると、リスト6.31のようになります。(本チュートリアルで初めて紹介したテクニックですが、このテクニックについては第10章でもう一度取り上げます。そこではコールバックを定義するときにメソッドを参照するという慣習について説明します。)
class User < ActiveRecord::Base
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.31のコードは、before_save
コールバックにブロックを渡してユーザーのメールアドレスを設定します。設定されるメールアドレスは、現在の値をStringクラスのdowncase
メソッドを使って小文字バージョンにしたものです。メールアドレスの小文字変換に対するテストは演習として残しておきます (6.5)。
リスト6.31では、次のように代入をしていましたが、
self.email = self.email.downcase
Userモデルの中では、右式でself
というキーワードは省略できます (ちなみにここのself
は現在のユーザーを指します)。したがって、次のように書くこともできます。
self.email = email.downcase
実は4.4.2のpalindrome
内でreverse
メソッドを使っていたときも、同様のケースであったことを思い出してください。そのときと同様で、左式ではself
を省略することはできません。したがって、
email = email.downcase
と書くとうまく動きません。(このトピックについては、8.4でより深く解説していきます。)
これで、先に述べたアリスのシナリオはうまくいくようになります。データベースは、最初のリクエストに基づいてユーザーのレコードを保存しますが、2度目の保存は一意性の制約に反するので拒否します(Railsのログにエラーが出力されますが、害は生じません)。さらに、インデックスをemail属性に追加したことで、6.1.4で挙げた2番目の目標である「多数のデータがあるときの検索効率を向上させる」も達成されました。これは、email
属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになったためです (コラム6.2)。
6.3 セキュアなパスワードを追加する
ユーザー属性の「名前」と「メールアドレス」に対してバリデーションを追加したので、最後の砦である「セキュアなパスワード」に取り掛かります。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存します。(ハッシュ化というと少し困惑してしまうかもしれません。4.3.3ではハッシュとはRubyのデータ構造であると説明しましたが、今回の「ハッシュ化」とはそういった構造ではなく、ハッシュ関数を使って入力されたデータを元に戻せない (不可逆な) データにする処理を指します。) また、入力されたパスワードを使用してユーザーを認証する手段と、第8章で使用する、ユーザーがサイトにログインできるようにする手段も提供します。
ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較、という手順で進んでいきます。比較の結果が一致すれば、送信されたパスワードは正しいと認識され、そのユーザーは認証されます。ここで、生のパスワードではなく、ハッシュ化されたパスワード同士を比較していることに注目してください。こうすることで、生のパスワードをデータベースに保存するという危険なことをしなくてもユーザーを認証できます。これで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれます。
6.3.1 ハッシュ化されたパスワード
セキュアなパスワードの実装は、has_secure_password
というRailsのメソッドを呼び出すだけでほとんど終わってしまいます。このメソッドは、Userモデルで次のように呼び出せます。
class User < ActiveRecord::Base
.
.
.
has_secure_password
end
上のようにモデルにこのメソッドを追加すると、次のような機能が使えるようになります。
- セキュアにハッシュ化したパスワードを、データベース内の
password_digest
という属性に保存できるようになる。 - 2つのペアの仮想的な属性18 (
password
とpassword_confirmation
)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。 -
authenticate
メソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalse
返すメソッド)。
この魔術的なhas_secure_password
機能を使えるようにするには、1つだけ条件があります。それは、モデル内にpassword_digest
という属性が含まれていることです。ちなみにdigestという言葉は、暗号化用ハッシュ関数という用語が語源です。したがって、今回の文脈ではハッシュ化されたパスワードと暗号化されたパスワードは似た表現となります19。今回はUserモデルで使うので、Userのデータモデルは以下の図のようになります (図6.8)。
図6.8のようなデータモデルにするために、まずはpassword_digest
カラム用の適切なマイグレーションを生成します。マイグレーション名は自由に指定できますが、次に示すように、末尾をto_users
にしておくことをお勧めします。こうしておくと、users
テーブルにカラムを追加するマイグレーションがRailsによって自動的に作成されるからです。add_password_digest_to_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.32)。
users
テーブルにpassword_digest
カラムを追加するマイグレーション db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
リスト6.32では、add_column
メソッドを使ってusers
テーブルにpassword_digest
カラムを追加しています。これを適用させるには、データベースでマイグレーションを実行します。
$ bundle exec rake db:migrate
また、has_secure_password
を使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcryptを使用するために、bcrypt-ruby
gemをGemfile
に追加します (リスト6.33)。
bcrypt
をGemfile
に追加する
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
.
.
.
次に、いつものようにbundle install
を実行します。
$ bundle install
6.3.2 ユーザーがセキュアなパスワードを持っている
Userモデルにpassword_digest
属性を追加し、Gemfileにbcryptを追加したことで、ようやくUserモデル内でhas_secure_password
が使えるようになりました (リスト6.34)。
has_secure_password
を追加する RED app/models/user.rb
class User < ActiveRecord::Base
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
end
リスト6.34でREDと記しておいたように、まだテストは落ちたままになっているはずです。コマンドラインで以下を実行して確認してください。
$ bundle exec rake test
テストが失敗する理由は、6.3.1で触れたようにhas_secure_password
には、仮想的なpassword
属性とpassword_confirmation
属性に対してバリデーションをする機能も(強制的に)追加されているからです。しかしリスト6.25のテストでは、@user
変数にこのような値がセットされておりません。
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
テストをパスさせるために、リスト6.36のようにパスワードとパスワード確認の値を追加します。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
end
これでテストが成功するようになります。
$ bundle exec rake test
Userモデルに対してhas_secure_password
を追加する利点は6.3.4で少しだけ説明しますが、 その前に、パスワードの最小文字数を設定する方法について説明します。
6.3.3 パスワードの最小文字数
パスワードを簡単に当てられないようにするために、パスワードの最小文字数を設定しておくことは一般に実用的です。Railsでパスワードの長さを設定する方法はたくさんありますが、今回は簡潔にパスワードが空でないことと最小文字数 (6文字) の2つを設定しましょう。パスワードの長さが6文字以上であることを検証するテストを、以下のリスト6.38に示します。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
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
end
ここで、以下のような多重代入 (Multiple Assignment) を使っていることに注目してください。
@user.password = @user.password_confirmation = "a" * 5
リスト6.38のこの行では、パスワードとパスワード確認に対して同時に代入をしています (このケースでは、リスト6.14と同じように、文字列の乗算を利用して5文字の文字列を代入しています)。
リスト6.16ではmaximum
を使ってユーザー名の最大文字数を制限していましたが、これと似たような形式のminimum
というオプションを使って、最小文字数のバリデーションを実装することができます。
validates :password, length: { minimum: 6 }
また、空のパスワードを入力させないために、存在性
のバリデーション (6.2.2) も一緒に追加します。結果として、Userモデルのコードはリスト6.39のようになります。(has_secure_passwordメソッドは存在性のバリデーションもしてくれるのですが、これは新しくレコードが追加されたときだけに適用されます。したがって、たとえばユーザーが " " (6文字分の空白スペース) といった文字列をパスワード欄に入力して更新しようとすると、バリデーションが適用されずに更新されてしまう問題が発生してしまいます。)
class User < ActiveRecord::Base
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
この時点で、テストは成功するはずです。
$ bundle exec rake test:models
6.3.4 ユーザーの作成と認証
以上でUserモデルの基本部分が完了しましたので、今度は7.1でユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを1人作成しましょう。また、Userモデルにhas_secure_password
を追加した効果についても (たとえばauthenticate
メソッドの効果なども) 具体的に見ていきます。
ただしWebからのユーザー登録はまだできない (第7章で完成させます) ので、今回はRailsコンソールを使ってユーザーを手動で作成することにしましょう。6.1.3で説明したcreate
を使いますが、後々実際のユーザーを作成する必要が出てくるので、今回はサンドボックス環境は使いません。したがって、今回作成したユーザーを保存すると、データベースに反映されます。それでは、まずrails console
コマンドを実行してセッションを開始し、次に有効な名前・メールアドレス・パスワード・パスワード確認を渡してユーザーを作成してみましょう。
$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?> password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-09-11 14:26:42", updated_at: "2014-09-11 14:26:42",
password_digest: "$2a$10$sLcMI2f8VglgirzjSJOln.Fv9NdLMbqmR4rdTWIXY1G...">
うまくデータベースに保存されたかどうかを確認するためには、開発環境用のデータベースをDB Browser for SQLiteで開き、 users
テーブルの中身を見ます (図6.9)20。もしCloud IDEを使っている場合は、データベースのファイルをダウンロードして開いてください (図6.5)。このとき、先ほど定義したUserモデルの属性 (図6.8) に対応したカラムがあることにも注目しておいてください
コンソールに戻ってpassword_digest
属性を参照してみると、リスト6.39のhas_secure_password
の効果を確認できます。
>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"
これは、Userオブジェクトを作成したときに、"foobar"
という文字列がハッシュ化された結果です。bcryptを使って生成されているので、この文字列から元々のパスワードを導出することは、コンピュータを使っても非現実的です21。
また6.3.1で説明したように、has_secure_password
をUserモデルに追加したことで、そのオブジェクト内でauthenticate
メソッドが使えるようになっています。このメソッドは、引数に与えられた文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digest
カラムの値を比較します。試しに、先ほど作成したuserオブジェクトに対して間違ったパスワードを与えてみましょう。
>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
間違ったパスワードを与えた結果、user.authenticate
がfalse
を返したことがわかります。次に、正しいパスワードを与えてみましょう。今度はauthenticate
がそのユーザーオブジェクトを返すようになります。
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-25 02:58:28", updated_at: "2014-07-25 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">
第8章では、このauthenticate
メソッドを使ってログインする方法を解説します。なお、authenticate
がUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrue
であることが重要です。Userオブジェクトはnil
でもfalse
でもないので、いい感じに仕事をしてくれています22。
>> !!user.authenticate("foobar")
=> true
6.4 最後に
この章では、ゼロからUserモデルを作成し、そこにname属性やemail属性、パスワード属性を加えました。また、それぞれの値を制限する多くの重要なバリデーションも追加しました。さらに、与えられたパスワードをセキュアに認証できる機能も実装しました。たった12行でここまでの機能が実装できたことは、(Railsの) 注目に値する点でもあります。
次の第7章では、ユーザーを作成するためのユーザー登録フォームを作成し、各ユーザーの情報を表示するためのページも作成します。第8章では、6.3の認証システムを利用して、ユーザーが実際にWebサイトにログインできるようにします。
Gitを使用している方は、しばらくコミットしていなかったのであれば、この時点でコミットしておくのがよいでしょう。
$ bundle exec rake test
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"
次にmasterブランチにマージして、リモートにあるリポジトリに対してpushします。
$ git checkout master
$ git merge modeling-users
$ git push
なお、本番環境でUserモデルを使うためには、heroku run
コマンドを使ってHeroku上でもマイグレーションを走らせる必要があります。
$ bundle exec rake test
$ git push heroku
$ heroku run rake db:migrate
うまくできたかどうかは、本番環境のコンソールに接続することで確認できます。
$ heroku run console --sandbox
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?> password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2014-08-29 03:27:50", updated_at: "2014-08-29 03:27:50",
password_digest: "$2a$10$IViF0Q5j3hsEVgHgrrKH3uDou86Ka2lEPz8zkwQopwj...">
6.4.1 本章のまとめ
- マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
- Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
- Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
- よくあるバリデーションには、存在性・長さ・フォーマットなどがある
- 正規表現は謎めいて見えるが非常に強力である
- データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
-
has_secure_password
メソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる
6.5 演習
注: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルブックのすべての演習の解答が掲載されており、www.railstutorial.orgで本書を購入いただいた方には無料で配布しています (訳注: 解答は英語です)。
演習とチュートリアル本編の食い違いを避ける方法については、3.6のトピックブランチの演習に追加したメモをご覧ください。
-
リスト6.31の、メールアドレスを小文字に変換するコードに対するテストを、リスト6.41に示されているように作成してください。このテストでは、
reload
メソッドを使用してデータベースから値を再度読み込み、assert_equal
メソッドを使用して同値であるかどうかをテストしてください。リスト6.41のテストが正しいかどうか検証するために、before_save
の行をコメントアウトするとテストが失敗し、元に戻すと成功することを確認してください。 -
before_save
コールバック内でemail.downcase!
と書き、email
属性を直接変更してもよいことを、テストスイートを走らせて確認してください (リスト6.42のように書いてもよいことを、テストスイートを実行して確認してください。 - 6.2.4で説明したように、 リスト6.21のメールアドレスチェックする正規表現は、“foo@bar..com”のようにドットが連続した無効なメールアドレスを許容してしまいます。このメールアドレスをリスト6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト6.43に示したもう少し複雑な正規表現を使用して、このテストがパスするようにしてください。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
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
test "email addresses should be saved as lower-case" do
mixed_case_email = "Foo@ExAMPle.CoM"
@user.email = mixed_case_email
@user.save
assert_equal mixed_case_email.downcase, @user.reload.email
end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?
end
end
before_save
コールバックの別の実装 GREEN app/models/user.rb
class User < ActiveRecord::Base
before_save { 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
class User < ActiveRecord::Base
before_save { email.downcase! }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[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
- この名前の由来は “active record pattern” です。Martin Fowler著「エンタープライズ アプリケーションアーキテクチャパターン 」で特定および命名されました。↑
- 「エスキューエル」と発音しますが、「スィークゥエル」もよく使われます。↑
- メールアドレスをユーザー名にしたことで、ユーザー同士で通信できるように拡張できる可能性が開かれます (第10章)。↑
-
t
オブジェクトが具体的に何をしているのかを正確に知る必要はありませんので、どうか心配しないでください。抽象化レイヤの素晴らしい点は、それが何であるかを知る必要がないという点です。安心してt
オブジェクトに仕事を任せればよいのです。↑ - 公式には「エスキューエライト (ess-cue-ell-ite)」と発音しますが、(本来は誤りとされている)「スィークゥエライト (sequel-ite)」もよく使われています。↑
- この唯一の例外が12.3.3に記されています。↑
-
"2014-07-24 00:57:46"
というタイムスタンプが気になった方もいると思いますが、著者はこの箇所を真夜中過ぎに書いたわけではありません。実はこのタイムスタンプは協定世界時 (UTC) に合わせてあります。これはグリニッジ標準時 (GMT) と同様、標準時間として使用されます。「NIST時刻と周波数FAQ」によると、問: 協定世界時 (Coordinated Universal Time) の略称がCUTではなくUTCなのはなぜですか 。答え: 協定世界時システムは、1970年に国際電気通信連合 (ITU) の技術専門家の国際諮問グループによって考案されました。このときITUは、混乱を最小限にとどめるために、略称を1つだけにしたいと考えました。このとき、英語式のCUTもフランス式のTUCも満場一致とならず、両者の妥協案としてUTCという略語が採用されました。↑ - 例外と例外ハンドリングは、ある意味でRubyの高度なテーマです。本書では例外についてこれ以上言及しません。しかし例外が重要なものであることも確かなので、12.4.1で推薦したRuby本で例外について詳しく学ぶことをおすすめします↑
-
update_attributes
メソッドはupdate
メソッドのエイリアスですが、単一属性を変更するupdate_attribute
メソッドとの違いを明確にするために、筆者は長いメソッド名の方を好んで使っています。↑ - 今後、コンソールコマンドの出力は、特に教育的効果が高いと思える場合 (ここでの
User.new
の場合など) を除いて省略いたします。↑ - 驚いたことに、公式標準によると、たとえば
"Michael Hartl"example.com
のようなクォートとスペースを使用したメールアドレスも有効なのだそうです。まったく馬鹿げています。↑ -
表6.1の正規表現の説明における「文字」は、実は「小文字のみ」が対象になっていることに注意してください。ただし、正規表現の末尾に
i
オプションを追加してあるので、大文字小文字が区別されずにマッチするようになっています。↑ - もしRubularのサービスが便利だと思ったら、素晴らしい功績を残した開発者であるMichael Lovittさんに報いるためにRubularへの寄付をお勧めします.。↑
- この節の冒頭で簡単に紹介したように、この目的に使用できる専用のテストデータベース
db/test.sqlite3
があります。↑ - 技術的には、メールアドレスのうちドメイン名部分だけが (本当は) 大文字小文字を区別しません。foo@bar.comは、本来はFoo@bar.comとは別のアドレスです。ただし現実的には、about.comでも指摘されているように、メールアドレスの大文字小文字を区別することを前提にするのはまずい方法です。「メールアドレスの大文字小文字を区別すると、果てしない混乱と相互運用性の問題とひどい頭痛が発生する。メールアドレスの入力時に大文字小文字の区別を要求するのは賢い方法とは言えない。現実には、メールアドレスの大文字小文字の区別を強制するメールサービスやISPはめったに存在しない。メールアドレスのすべての文字を大文字にするなど、受信者のメールアドレスが誤って入力されていれば、メールは返送されるだけだ。」Riley Mosesによるご指摘に感謝いたします。↑
- もちろん、リスト6.2の
users
テーブル用のマイグレーションファイルを単に編集することも可能なのですが、その場合ロールバックが必要となり、マイグレーションが戻ってしまいます。データモデルの変更が必要になったらその都度マイグレーションを行うのがRails流です。↑ - 他にどんなコールバックがあるのか知りたい場合は、Rails APIのコールバック (英語) を読んでみてください。↑
- ここでいう「仮想的 (Virtual)」とは、Userモデルのオブジェクトからは存在しているように見えるが、データベースには対応するカラムが存在しない、という意味です。↑
- ハッシュ化されたパスワードは、暗号化されたパスワードとよく誤解されがちです。たとえば、(実は本書の第1版や第2版でも間違っていたのですが)
has_secure_password
のソースコードでもこの手の間違いがあります。というのも、専門用語としての「暗号」というのは、設計上元に戻すことができることを指します (暗号化できるという文には、復号もできるというニュアンスが含まれます)。一方、「パスワードのハッシュ化」では元に戻せない (不可逆) という点が重要になります。したがって、 「計算量的に元のパスワードを復元するのは困難である」という点を強調するために、暗号化ではなくハッシュ化という用語を使っています。(この間違った用語について指摘してくれたAndy Philipsに感謝します。)↑ -
もしうまくいかなくても、いつでもデータベースの中身をリセットできるので安心してください。リセットしたい場合は、以下の手順を踏んでください。
- まずはコンソールから脱出してください (Ctrl-C)
- 次に、コマンドラインから
$ rm -f development.sqlite3
を実行してデータベースの中身を削除してください(第7章でもっと便利なメソッドを紹介します) -
$ bundle exec rake db:migrate
コマンドを実行して、もう一度マイグレーションを走らせてください - 再度Railsコンソールを開き、コンソール上での作業をもう一度やり直してみてください
- 設計上、bcryptアルゴリズムではハッシュ化する前にソルトを追加しています。これにより、辞書攻撃 (Dictionary Attacks) やレインボーテーブル攻撃 (Rainbow Table Attacks) といったタイプの攻撃を防ぐことができます。↑
-
4.2.3で、
!!
という式が対応する論理値オブジェクト (!!nil => false) に変換されていたことを思い出してください。↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!