Ruby on Rails チュートリアル
-
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
|
||
第4版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
第6章ユーザーのモデルを作成する
第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わりました (5.4)。これから6つの章を通して、ユーザー登録ページを作っていくことにしましょう。本章では、一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいきます。第7章では、ユーザーがサイトにユーザー登録できるようにし、ユーザープロフィールのためのページを作成します。ユーザー登録できるようになったら、ログインやログアウトをできる仕組みを作り (第8章と第9章)、第10章からは不正なアクセスを取り扱う方法について学んでいきます (10.2.1)。最後に第11章と第12章でメールアドレスを使ってアカウントを有効化する方法と、パスワードを再設定する方法について学びます。まとめると、第6章から第12章を通して、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では、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model) と呼びます (1.3.3で言うMVCのMのことです)。Railsでは、データを永続化するデフォルトの解決策として、データベースを使ってデータを長期間保存します。また、データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ばれます1。Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っています。これらのメソッドを使うのに、リレーショナルデータベースで使うSQL (Structured Query Language)2 を意識する必要はありません。さらに、Railsにはマイグレーション (Migration) という機能があります。データの定義をRubyで記述することができ、SQLのDDL (Data Definition Language) を新たに学ぶ必要がありません。つまりRailsは、データベースの細部をほぼ完全に隠蔽し、切り離してくれます。実際、本書ではSQLiteを開発 (development) 環境で使い、また、PostgreSQLを (Herokuでの) 本番環境 (production) で使います (1.5) が、本番環境のデータの保存方法の詳細について考える必要はほとんどありません。
Gitでバージョン管理を行なっているのであれば、このタイミングでユーザーをモデリングするためのトピックブランチを作成しておいてください。
$ 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.17では、次のように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.38で、ユーザーコントローラ (と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/20160523010738_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.6やリスト 5.38でアクション名を使って生成した例と比較してみてください。
リスト 6.1にあるgenerate
コマンドの結果のひとつとして、マイグレーションと呼ばれる新しいファイルが生成されます。マイグレーションは、データベースの構造をインクリメンタルに変更する手段を提供します。それにより、要求が変更された場合にデータモデルを適合させることができます。このUserモデルの例の場合、マイグレーションはモデル生成スクリプトによって自動的に作られました。リスト 6.2に示したように name
とemail
の2つのカラムを持つusers
テーブルを作成します (6.2.5で、マイグレーションを1から手動で作成する方法について説明します)。
users
テーブルを作るための) Userモデルのマイグレーション db/migrate/[timestamp]_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
マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加されます。以前はインクリメンタルな整数が追加されましたが、複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、コンフリクトを引き起こしていました。現在のタイムスタンプによる方法であれば、まったく同時にマイグレーションが生成されるという通常ではありえないことが起きない限り、そのようなコンフリクトは避けられます。
マイグレーション自体は、データベースに与える変更を定義した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つの「マジックカラム (Magic Columns)」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプです (このマジックカラムの使用例を6.1.3から具体的に見ていきます)。リスト 6.2のマイグレーテョンによって作成された完全なデータモデルを図 6.4に示します (図 6.3のスケッチには無かったマジックカラムが追加されています)。
マイグレーションは、次のようにdb:migrate
コマンド を使って実行することができます。これを「マイグレーションの適用 (migrating up)」と呼びます。
$ rails db:migrate
(2.2で、このコマンドを似たような状況で実行したことを思い出してみてください) 。初めてdb:migrate
が実行されると、db/development.sqlite3
という名前のファイルが生成されます。これはSQLite5データベースの実体です。development.sqlite3
ファイルを開くためのDB Browser for SQLiteという素晴らしいツールを使うと、データベースの構造を見ることができます (Cloud IDEを使っている場合は、図 6.5のようにまずはファイルをお手元にダウンロードする必要があります)。結果は図 6.6のようになるので、図 6.4と比べて見てください。図 6.6の中に、id
というマイグレーションのときに説明されなかったカラムの存在に気づいたかもしれません。2.2で簡単に説明したとおり、このカラムは自動的に作成され、Railsが各行を一意に識別するために使います。
演習
- Railsは
db/
ディレクトリの中にあるschema.rb
というファイルを使っています。これはデータベースの構造 (スキーマ (schema) と呼びます) を追跡するために使われます。さて、あなたの環境にあるdb/schema.rb
の内容を調べ、その内容とマイグレーションファイル (リスト 6.2) の内容を比べてみてください。 - ほぼすべてのマイグレーションは、元に戻すことが可能です (少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック (rollback)と呼び、Railsでは
db:rollback
というコマンドで実現できます。上のコマンドを実行後、$ rails db:rollback
db/schema.rb
の内容を調べてみて、ロールバックが成功したかどうか確認してみてください (コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_table
コマンドを内部で呼び出しています。これがうまくいくのは、drop_table
とcreate_table
がそれぞれ対応していることをchange
メソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、change
メソッドの代わりに、up
とdown
のメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。 - もう一度
rails db:migrate
コマンドを実行し、db/schema.rb
の内容が元に戻ったことを確認してください。
6.1.2 modelファイル
これまで、リスト 6.1のUserモデルの作成によってどのように (リスト 6.1の) マイグレーションファイルが作成されるかを見てきました。そして図 6.6でこのマイグレーションを実行した結果を見ました。users
テーブルを作成することで、development.sqlite3
という名のファイルを更新し、id
、name
、email
、created_at
、updated_at
を作成しました。また、リスト 6.1ではモデル用のuser.rbも作られました。この節では、以後このモデル用ファイルを理解することに専念します。
まずはapp/models/
ディレクトリにある、user.rb
に書かれたUserモデルのコードを見てみましょう。これは控えめに言ってもとてもよくまとまっているコードです (リスト 6.3)。
class User < ApplicationRecord
end
4.4.2で学んだことを思い出してみましょう。class User < ApplicationRecord
という構文で、User
クラスはApplicationRecord
を継承するので、Userモデルは自動的にActiveRecord::Base
クラスのすべての機能を持つことになります (図 2.18)。とはいえ、継承されていることが分かっていても、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.17の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.1ms) SAVEPOINT active_record_1
SQL (0.8ms) INSERT INTO "users" ("name", "email", "created_at",
"updated_at") VALUES (?, ?, ?, ?) [["name", "Michael Hartl"],
["email", "mhartl@example.com"], ["created_at", 2016-05-23 19:05:58 UTC],
["updated_at", 2016-05-23 19:05:58 UTC]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> 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: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
上の結果から、id
には1
という値が代入され、一方でマジックカラムには現在の日時が代入されているのがわかります7。現在、作成と更新のタイムスタンプは同一ですが、更新するようになると (6.1.5) これらの値が異なっていきます。
4.4.5のUserクラスと同様に、Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます。
>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Mon, 23 May 2016 19:05:58 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:
"2016-05-23 19:18:46", updated_at: "2016-05-23 19:18:46">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
User.create
は、true
かfalse
を返す代わりに、ユーザーオブジェクト自身を返すことに注目してください。返されたユーザーオブジェクトは (上の2つ目のコマンドにあるfoo
のように) 変数に代入することもできます。
destroy
はcreate
の逆です。
>> foo.destroy
(0.1ms) SAVEPOINT active_record_1
SQL (0.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
create
と同じようにdestroy
はそのオブジェクト自身を返しますが、その戻り値を使ってもう一度destroy
を呼ぶことはできません。さらに、削除されたオブジェクトは次のようにまだメモリ上に残っています。
>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2016-05-23
19:19:06", updated_at: "2016-05-23 19:19:06">
では、オブジェクトが本当に削除されたかどうかをどのようにして知ればよいのでしょうか。そして、保存して削除されていないオブジェクトの場合、どうやってデータベースからユーザーを取得するのでしょうか。これらの問いに答えるためには、Active Recordを使ってUserオブジェクトを検索する方法について学ぶ必要があります。
演習
user.name
とuser.email
が、どちらもString
クラスのインスタンスであることを確認してみてください。created_at
とupdated_at
は、どのクラスのインスタンスでしょうか?
6.1.4 ユーザーオブジェクトを検索する
Active Recordには、オブジェクトを検索するための方法がいくつもあります。これらの機能を使って、過去に作成した最初のユーザーを探してみましょう。また、3番目のユーザー (foo
) が削除されていることを確認しましょう。まずは存在するユーザーから探してみましょう。
>> User.find(1)
=> #<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.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: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
これまでメールアドレスをユーザー名として使ってきたので、このようなfind
関連メソッドは、ユーザーをサイトにログインさせる方法を学ぶときに役に立ちます (第7章)。ユーザー数が膨大になるとfind_by
では検索効率が低下するのではないかと心配する方もいるかもしれませんが、あせる必要はありません。この問題およびデータベースのインデックスを使った解決策については6.2.5で扱います。
ユーザーを検索する一般的な方法をあと少しだけご紹介して、この節を終わりにすることにしましょう。まず初めにfirst
メソッドです。
>> User.first
=> #<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">
読んで字のごとく、first
は単にデータベースの最初のユーザーを返します。次はall
メソッドです。
>> User.all
=> #<ActiveRecord::Relation [#<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 id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>
コンソールの出力結果を見ると、User.all
でデータベースのすべてのUserオブジェクトが返ってくることがわかります。また、返ってきたオブジェクトのクラスがActiveRecord::Relation
となっています。これは、各オブジェクトを配列として効率的にまとめてくれるクラスです (4.3.1)。
演習
name
を使ってユーザーオブジェクトを検索してみてください。また、find_by_name
メソッドが使えることも確認してみてください (古いRailsアプリケーションでは、古いタイプのfind_by
をよく見かけることでしょう)。- 実用的な目的のため、
User.all
はまるで配列のように扱うことができますが、実際には配列ではありません。User.all
で生成されるオブジェクトを調べ、Array
クラスではなくUser::ActiveRecord_Relation
クラスであることを確認してみてください。 User.all
に対してlength
メソッドを呼び出すと、その長さを求められることを確認してみてください (4.2.3)。Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質があります。これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」。(訳注: そういえばRubyKaigi 2016の基調講演で、Ruby作者のMatzがダックタイピングについて説明していました。2〜3分の短くて分かりやすい説明なので、ぜひ視聴してみてください!)
6.1.5 ユーザーオブジェクトを更新する
いったんオブジェクトを作成すれば、今度は何度でも更新したくなるものです。基本的な更新の方法は2つです。ひとつは、4.4.5でやったように属性を個別に代入する方法です。
>> 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を実行する必要があることを忘れないでください。保存を行わずに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
=> "2016-05-23 19:05:58"
>> user.updated_at
=> "2016-05-23 19:08:23"
属性を更新するもう1つの方法は、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, "El Duderino")
=> true
>> user.name
=> "El Duderino"
演習
- userオブジェクトへの代入を使ってname属性を使って更新し、
save
で保存してみてください。 - 今度は
update_attributes
を使って、email属性を更新および保存してみてください。 - 同様にして、マジックカラムである
created_at
も直接更新できることを確認してみてください。ヒント: 更新するときは「1.year.ago
」を使うと便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の時間を算出してくれます。
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モデルにはまだバリデーションがないので、このテストは成功するはずです。
$ rails test:models
上ではrails test:models
というコマンドを実行していますが、これはモデルに関するテストだけを走らせるコマンドです (5.3.4で使ったrails 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
この時点では、モデルのテストは redになっているはずです。
$ rails test:models
第2章の演習で少し触れましたが、name属性の存在を検査する方法は、リスト 6.9に示したとおり、validates
メソッドにpresence: true
という引数を与えて使うことです。presence: true
という引数は、要素が1つのオプションハッシュです。4.3.4のようにメソッドの最後の引数としてハッシュを渡す場合、波カッコを付けなくても問題ありません (5.1.1でも説明したように、Railsのオプションハッシュは繰り返し登場するテーマです)。
name
属性の存在性を検証するgreen app/models/user.rb
class User < ApplicationRecord
validates :name, presence: true
end
リスト 6.9は一見魔法のように見えるかもしれませんが、validates
は単なるメソッドです。カッコを使ってリスト 6.9を同等のコードに書き換えたものを次に示します。
class User < ApplicationRecord
validates(:name, presence: true)
end
コンソールを起動して、Userモデルに検証を追加した効果を見てみましょう10。
$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false
このように、user
変数が有効かどうかをvalid?
メソッドでチェックすることができます。もしオブジェクトが1つ以上の検証に失敗したときは、false
を返します。また、すべてのバリデーションに通ったときにtrue
を返します。今回の場合、検証が1つしかないので、どの検証が失敗したかわかります。しかし、失敗したときに作られるerrors
オブジェクトを使って確認すれば、さらに便利です。
>> user.errors.full_messages
=> ["Name can't be blank"]
(Railsが属性の存在性を検査するときに、エラーメッセージはヒントになります。これにはblank?
メソッドを用います。4.4.3の終わりに見ました)。
Userオブジェクトは有効ではなくなったので、データベースに保存しようとすると自動的に失敗するはずです。
>> user.save
=> false
この変更によりリスト 6.7のテストは greenしているはずです。
$ rails 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 < ApplicationRecord
validates :name, presence: true
validates :email, presence: true
end
これですべての存在性がチェックされたので、テストスイートは greenするはずです。
$ rails test
演習
- 新しいユーザー
u
を作成し、作成した時点では有効ではない (invalid) ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。 u.errors.messages
を実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
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"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
a@example.com"
>> ("a" * 244 + "@example.com").length
=> 256
この時点では、リスト 6.14のテストは redになっているはずです。
$ rails test
これをパスさせるためには、長さを強制するための検証の引数を使う必要があります。:maximum
パラメータと共に用いられる:length
は、長さの上限を強制します (リスト 6.16)。
name
属性に長さの検証を追加するgreen app/models/user.rb
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
end
これでテストが greenになるはずです。
$ rails test
成功したテストスイートを再利用して、今度は少し難しい、メールアドレスのフォーマット検証作業に取りかかりましょう。
演習
- 長すぎるnameとemail属性を持ったuserオブジェクトを生成し、有効でないことを確認してみましょう。
- 長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
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
この時点では、テストは redするはずです。
$ rails 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を含めずに試してみてください。こうすることで、複数のメールアドレスを1回で検知できるようになり便利です。また、Rubularではスラッシュ /.../
の内側の部分を書くだけで大丈夫ですので、両端付近にあるスラッシュは取り除いてください。)
表 6.1の正規表現を適用してemail
のフォーマットを検証した結果を、リスト 6.21に示します。
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では定数を意味します。次のコードは、
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.21のように正規表現を修正すると直りますが、これについては演習 (6.2.4.1) に持ち越すことにしましょう (訳注: ドットの連続だけでなく、国際化ドメインにも対応できた方が良いでしょう。指摘してくれた@znzさん、ありがとうございます!)。
この時点では、テストは greenになるはずです。
$ rails test:models
残る制約は、メールアドレスが一意であることを強制することだけとなりました。
演習
- リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
- 先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
- foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
class User < ApplicationRecord
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 }
end
6.2.5 一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validates
メソッドの:unique
オプションを使います。ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。モデルのテストではこれまで、主にUser.new
を使ってきました。このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14。そのため、まずは重複したメールアドレスからテストしていきます (リスト 6.24)。
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.24のテストをパスさせるために、email
のバリデーションにuniqueness: true
というオプションを追加します リスト 6.25。
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
実装の途中ですが、ここで1つ補足します。通常、メールアドレスでは大文字小文字が区別されません。すなわち、foo@bar.com
はFOO@BAR.COM
やFoO@BAr.coM
と書いても扱いは同じです。したがって、メールアドレスの検証ではこのような場合も考慮する必要があります15。この性質のため、大文字を区別しないでテストすることが重要になり、実際のテストコードはリスト 6.26のようにしなければなりません。
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.27)。
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
ここで、リスト 6.25のtrue
をcase_sensitive: false
に置き換えただけであることに注目してください (リスト 6.27)。Railsはこの場合、:uniqueness
をtrue
と判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。
$ rails 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属性と渡されたメールアドレスを比較するという非効率的な方法しかありません。つまり、例えばデータベース上の最後のユーザを探す場合でも、すべてのユーザーを最初から順に一人ずつ探していくことになります。これは、データベースの世界では全表スキャン (Full-table Scan) として知られており、数千のユーザーがいる実際のサイトでは極めて不都合です。
emailカラムにインデックスを追加することで、この問題を解決することができます。データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。索引のない本では、渡された言葉 (例えば、"foobar") が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります (紙バージョンの全表スキャン)。しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から探すだけで済みます。データベースのインデックスも本質的には本の索引と同じように動作します。
emailインデックスを追加すると、データモデリングの変更が必要になります。Railsでは (6.1.1で見たように) マイグレーションでインデックスを追加します。6.1.1で、Userモデルを生成すると自動的に新しいマイグレーションが作成されたことを思い出してください (リスト 6.2)。今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigration
ジェネレーターを使ってマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。リスト 6.29のように定義を記述する必要があります16。
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
def change
add_index :users, :email, unique: true
end
end
上のコードでは、users
テーブルのemail
カラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っています。インデックス自体は一意性を強制しませんが、オプションでunique: true
を指定することで強制できるようになります。
最後に、データベースをマイグレートします。
$ rails db:migrate
(上のコマンドが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了してみてください。そのセッションがデータベースをロックしてマイグレーションを妨げている可能性があります。)
この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です (リスト 6.30) (実はこのデータはいずれも有効ではありませんが、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.31)。
# 空にする (既存のコードは削除する)
これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。それは、いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処です。例えば、Foo@ExAMPle.Comとfoo@example.comが別々の文字列だと解釈してしまうデータベースがありますが、私達のアプリケーションではこれらの文字列は同一であると解釈されるべきです。この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採ります。例えば"Foo@ExAMPle.CoM"という文字列が渡されたら、保存する直前に"foo@example.com"に変換してしまいます。これを実装するためにActive Recordのコールバック (callback) メソッドを利用します。このメソッドは、ある特定の時点で呼び出されるメソッドです。今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_save
というコールバックを使います。これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します17。作成したコードをリスト 6.32に示します。(本チュートリアルで初めて紹介したテクニックですが、このテクニックについては11.1でもう一度取り上げます。そこではコールバックを定義するときにメソッドを参照するという慣習について説明します。)
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.32のコードは、before_save
コールバックにブロックを渡してユーザーのメールアドレスを設定します。設定されるメールアドレスは、現在の値をStringクラスのdowncase
メソッドを使って小文字バージョンにしたものです (メールアドレスの小文字化に対するテストは、演習6.2.5.1に回します)。
また、リスト 6.32では次のように書くこともできましたが、
self.email = self.email.downcase
Userモデルの中では右式のself
を省略できるので、今回は次のように書きました (ちなみにこのself
は現在のユーザーを指します)。
self.email = email.downcase
実は4.4.2のpalindrome
内でreverse
メソッドを使っていたときも、同様のケースであったことを思い出してください。そのときと同様で、左式ではself
を省略することはできません。したがって、
email = email.downcase
と書くとうまく動きません。(このトピックについては、9.1でより深く解説していきます。)
これで、先に述べたアリスのシナリオはうまくいくようになります。データベースは、最初のリクエストに基づいてユーザーのレコードを保存しますが、2度目の保存は一意性の制約に反するので拒否します(Railsのログにエラーが出力されますが、害は生じません)。さらに、インデックスをemail属性に追加したことで、6.1.4で挙げた2番目の目標である「多数のデータがあるときの検索効率を向上させる」も達成されました。これは、email
属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになったためです (コラム 6.2)。
演習
- リスト 6.33を参考に、メールアドレスを小文字にするテストをリスト 6.32に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新する
reload
メソッドと、値が一致しているかどうか確認するassert_equal
メソッドを使っています。リスト 6.33のテストがうまく動いているか確認するためにも、before_save
の行をコメントアウトして redになることを、また、コメントアウトを解除すると greenになることを確認してみましょう。 - テストスイートの実行結果を確認しながら、
before_save
コールバックをemail.downcase!
に書き換えてみましょう。ヒント: メソッドの末尾に!
を付け足すことにより、email
属性を直接変更できるようになります (リスト 6.34)。
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
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
end
class User < ApplicationRecord
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 }
end
6.3 セキュアなパスワードを追加する
ユーザー属性の「名前」と「メールアドレス」に対してバリデーションを追加したので、最後の砦である「セキュアなパスワード」に取り掛かります。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存します。いきなりハッシュ化と言われると少し困惑してしまうかもしれません。4.3.3ではハッシュとはRubyのデータ構造であると説明しましたが、今回の「ハッシュ化」とはそういった構造ではなく、ハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにする処理を指します。なお、このハッシュ化されたパスワードは、第8章のログイン機構でユーザーを認証 (authenticate) する際に利用します。
ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較、という手順で進んでいきます。比較の結果が一致すれば、送信されたパスワードは正しいと認識され、そのユーザーは認証されます。ここで、生のパスワードではなく、ハッシュ化されたパスワード同士を比較していることに注目してください。こうすることで、生のパスワードをデータベースに保存するという危険なことをしなくてもユーザーを認証できます。これで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれます。
6.3.1 ハッシュ化されたパスワード
セキュアなパスワードの実装は、has_secure_password
というRailsのメソッドを呼び出すだけでほとんど終わってしまいます。このメソッドは、Userモデルで次のように呼び出せます。
class User < ApplicationRecord
.
.
.
has_secure_password
end
上のようにモデルにこのメソッドを追加すると、次のような機能が使えるようになります。
- セキュアにハッシュ化したパスワードを、データベース内の
password_digest
という属性に保存できるようになる。 - 2つのペアの仮想的な属性 (
password
とpassword_confirmation
) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される18 。 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.35)。
password_digest
カラムを追加するマイグレーション db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.0]
def change
add_column :users, :password_digest, :string
end
end
リスト 6.35では、add_column
メソッドを使ってusers
テーブルpassword_digest
カラムを追加しています。これを適用させるには、データベースでマイグレーションを実行します。
$ rails db:migrate
また、has_secure_password
を使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcrypt
を使うために、bcrypt
gemをGemfile
に追加します (リスト 6.36)20。
bcrypt
をGemfile
に追加する
source 'https://rubygems.org'
gem 'rails', '5.0.3'
gem 'bcrypt', '3.1.11'
.
.
.
次に、いつものようにbundle install
を実行します。
$ bundle install
6.3.2 ユーザーがセキュアなパスワードを持っている
Userモデルにpassword_digest
属性を追加し、Gemfileにbcryptを追加したことで、ようやくUserモデル内でhas_secure_password
が使えるようになりました (リスト 6.37)。
has_secure_password
を追加する red 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
end
リスト 6.37で redと記しておいたように、まだテストは落ちたままになっているはずです。コマンドラインからテストを実行して確認してみてください。
$ rails test
テストが失敗する理由は、6.3.1で触れたようにhas_secure_password
には、仮想的なpassword
属性とpassword_confirmation
属性に対してバリデーションをする機能も(強制的に)追加されているからです。しかしリスト 6.26のテストでは、@user
変数にこのような値がセットされておりません。
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
テストをパスさせるために、リスト 6.39のようにパスワードとパスワード確認の値を追加します。
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
setup
メソッド内の1行目の末尾で、カンマ ,
を追加している点に注意してください。これはRubyの文法的に必須となるカンマです (4.3.3)。試しにこのカンマを消してみると、文法エラー (Syntax Error) と表示されるはずです。逆に言えば、本書を進めていく途中で文法エラーが表示された場合、どこかしらの文法が間違っているということになります。今後、エラーが出たときに動じないために、どんなときにどんなエラーが発生するのかを覚えていくと良いでしょう (コラム 1.1)。
話を戻して、上の変更でテストが greenになるはずです。
$ rails test
Userモデルに対してhas_secure_password
を追加する利点は6.3.4で少しだけ説明しますが、 その前に、パスワードの最小文字数を設定する方法について説明します。
演習
- この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、
valid?
で失敗してしまうことを確認してみてください。 - なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
6.3.3 パスワードの最小文字数
パスワードを簡単に当てられないようにするために、パスワードの最小文字数を設定しておくことは一般に実用的です。Railsでパスワードの長さを設定する方法はたくさんありますが、今回は簡潔にパスワードが空でないことと最小文字数 (6文字) の2つを設定しましょう。パスワードの長さが6文字以上であることを検証するテストを、次のリスト 6.41に示します。
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.41で使われていました。パスワードとパスワード確認に対して同時に代入をしています (このケースでは、リスト 6.14と同じように、文字列の乗算を利用して5文字の文字列を代入しています)。
リスト 6.16ではmaximum
を使ってユーザー名の最大文字数を制限していましたが、これと似たような形式のminimum
というオプションを使って、最小文字数のバリデーションを実装することができます。
validates :password, length: { minimum: 6 }
また、空のパスワードを入力させないために、存在性のバリデーション (6.2.2) も一緒に追加します。結果として、Userモデルのコードはリスト 6.42のようになります。ちなみにhas_secure_password
メソッドは存在性のバリデーションもしてくれるのですが、これは新しくレコードが追加されたときだけに適用される性質を持っています。したがって、例えばユーザーが ’ ’
(6文字分の空白スペース) といった文字列をパスワード欄に入力して更新しようとすると、バリデーションが適用されずに更新されてしまう問題が発生してしまうのです。
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
この時点では、テストは greenになるはずです。
$ rails test:models
演習
- 有効な名前とメールアドレスでも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
- 上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
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: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">
うまくデータベースに保存されたかどうかを確認するために、開発環境用のデータベースをDB Browser for SQLiteで開き、users
テーブルの中身を見てみましょう (図 6.9)21。もしCloud IDEを使っている場合は、データベースのファイルをダウンロードして開いてください (図 6.5)。このとき、先ほど定義したUserモデルの属性 (図 6.8) に対応したカラムがあることにも注目しておいてください。
コンソールに戻ってpassword_digest
属性を参照してみると、リスト 6.42のhas_secure_password
の効果を確認できます。
>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3yOcVFzb6oK"
これは、Userオブジェクトを作成したときに、"foobar"
という文字列がハッシュ化された結果です。bcryptを使って生成されているので、この文字列から元々のパスワードを導出することは、コンピュータを使っても非現実的です22。
また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: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">
第8章では、このauthenticate
メソッドを使ってログインする方法を解説します。なお、authenticate
がUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrue
であることが重要です。4.2.3で紹介した、!!
でそのオブジェクトが対応する論理値オブジェクトに変換できることを思い出してください。この性質を利用すると、user.authenticate
がいい感じに仕事をしてくれるようになります。
>> !!user.authenticate("foobar")
=> true
6.4 最後に
この章では、ゼロからUserモデルを作成し、そこにname属性やemail属性、パスワード属性を加えました。また、それぞれの値を制限する多くの重要なバリデーションも追加しました。さらに、渡されたパスワードをセキュアに認証できる機能も実装しました。たった12行でここまでの機能が実装できたことは、Railsの注目に値する特長でもあります。
次の第7章では、ユーザーを作成するためのユーザー登録フォームを作成し、各ユーザーの情報を表示するためのページも作成します。第8章では、6.3の認証システムを利用して、ユーザーが実際にWebサイトにログインできるようにします。
さて、Gitでしばらくコミットしていなかったのであれば、この時点でコミットしておきましょう。
$ rails 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上でもマイグレーションを走らせる必要があります。
$ rails test
$ git push heroku
$ heroku run rails db:migrate
うまくできたかどうかは、本番環境のコンソールに接続することで確認できます。
$ heroku run rails 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: "2016-05-23 20:54:41", updated_at: "2016-05-23 20:54:41",
password_digest: "$2a$10$74xFguZRoTZBXTUqs1FjpOf3OoLhrvgxC2wlohtTEcH...">
6.4.1 本章のまとめ
- マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
- Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
- Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
- よくあるバリデーションには、存在性・長さ・フォーマットなどがある
- 正規表現は謎めいて見えるが非常に強力である
- データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
has_secure_password
メソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる
- この名前の由来は「Active Record Pattern」です。Martin Fowler著「エンタープライズ アプリケーションアーキテクチャパターン 」で特定および命名されました。 ↑
- 「エスキューエル」と発音しますが、「スィークゥエル」もよく使われます。ちなみに文中から執筆者がどちらを好んでいるかを判別する方法があります。もし “an SQL database” と書いていれば前者を、“a SQL database” と書いていれば後者を好んでいるといった具合です。ちなみに私 (筆者) は後者の呼び方を好んでいます。 ↑
- メールアドレスをユーザー名にしたことで、ユーザー同士で通信できるように拡張できる可能性が開かれます (第11章と第12章)。 ↑
t
オブジェクトが具体的に何をしているのかを正確に知る必要はありませんので、どうか心配しないでください。抽象化レイヤの素晴らしい点は、それが何であるかを知る必要がないという点です。安心してt
オブジェクトに仕事を任せればよいのです。 ↑- 公式には「エスキューエライト (ess-cue-ell-ite)」と発音しますが、(本来は誤りとされている)「スィークゥエライト (sequel-ite)」もよく使われています。 ↑
- この唯一の例外が14.3.3に記されています。 ↑
- このタイムスタンプは協定世界時 (UTC) に合わせてあります。これはグリニッジ標準時 (GMT) と同様、標準時間として使われます。「NIST時刻と周波数FAQ」によると、問: 協定世界時 (Coordinated Universal Time) の略称がCUTではなくUTCなのはなぜですか 。答え: 協定世界時システムは、1970年に国際電気通信連合 (ITU) の技術専門家の国際諮問グループによって考案されました。このときITUは、混乱を最小限にとどめるために、略称を1つだけにしたいと考えました。このとき、英語式のCUTもフランス式のTUCも満場一致とならず、両者の妥協案としてUTCという略語が採用されました。 ↑
- 例外と例外ハンドリングは、Rubyの高度なテーマです。本書では例外についてこれ以上言及しません。しかし例外が重要なものであることも確かなので、14.4.2で紹介する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に感謝します。) ↑ - これまでと同様に、Gemfileで指定した各gemのバージョンはgemfiles-4th-ed.railstutorial.orgと一致している必要があります。もしうまく次に進めない場合はチェックしてみてください。 ↑
もしうまくいかなくても、いつでもデータベースの中身をリセットできるので安心してください。リセットしたい場合は、次の手順を踏んでください。
- まずはコンソールから脱出してください (Ctrl-C)
- 次に、コマンドラインから
$ rm -f development.sqlite3
を実行してデータベースの中身を削除してください (第7章でもっと便利なメソッドを紹介します) -
$ rails db:migrate
コマンドを実行して、もう一度マイグレーションを走らせてください - 再度Railsコンソールを開き、コンソール上での作業をもう一度やり直してみてください
- 設計上、bcryptアルゴリズムではハッシュ化する前にソルト化されたハッシュ (Salted Hash) を追加しています。これにより、辞書攻撃 (Dictionary Attacks) やレインボーテーブル攻撃 (Rainbow Table Attacks) といったタイプの攻撃を防ぐことができます。 ↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!