Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
- 第12章Rails 4.0へのアップグレード
|
||
第2版 目次
|
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
- 第12章Rails 4.0へのアップグレード
第6章ユーザーのモデルを作成する
第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わりました (5.4)。これから4つの章を通して、ユーザー登録ページを作っていくことにしましょう。最初の一番重要なステップは、サイトのユーザー用のデータモデルの作成と、データを保存する手段の確保です。第7章では、ユーザーがサイトにユーザー登録できるようにし、ユーザープロファイルのためのページを作成します。ユーザー登録ができるようになると、次にサインイン、サインアウトもできるようにします (第8章)。そして第9章 (9.2.1) では、不正なアクセスから守る方法を学びます。まとめると、第6章から第9章を通して、Railsのログインと認証のシステムを一通り開発します。ご存知の方もいるとは思いますが、Railsでは既にさまざまな認証方法が利用可能です。コラム 6.1では、最初に少なくとも一度は自分で認証システムを作ってみることをお勧めする理由について説明しています。
この章は長いうえに、学ぶことがたくさんあります。特に、これまでデータモデリングをしたことがない人にとっては、もしかすると、これまでとは違った難しさを感じるかもしれません。しかし、この章が終わるまでには、ユーザー情報の検証、保存、取得ができる極めて強力なシステムを作成します。
事実上、すべてのアプリは何らかのログイン/認証システムを必要とします。そのため、多くのWebフレームワークではこのようなログイン/認証システムを実装するための選択肢が多数提供されています。Railsもまた例外ではありません。認証 (authentication) と認可 (authorization) のシステムの例だと、Clearance、Authlogic、Devise、CanCanなどがあります (Railsに限らなければOpenIDやOAuthの上に構築する方法もあります)。 なぜ車輪の再発明をするのか、という質問があるのも当然です。自分でわざわざ作らなくても、いつも使える方法をただ利用するだけではいけないのでしょうか。
ある実践的な実験によると、多くのサイトの認証システムは膨大なカスタマイズを必要とするため、サードパーティ製品を変更して導入する場合にはシステムをゼロから作成するよりも多くの仕事を要するという結果が出ています。加えて、既成品のシステムは内部がわかりづらいことが多く、ブラックボックスになっています。自分で作成したシステムであれば、それをとてもよく理解しているはずです。さらに言えば、最近のRailsへの変更 (6.3) により、カスタム認証システムを容易に作成できるようになりました。最後に、あえて最終的にサードパーティの認証システムを導入することになったとしても、自分自身で認証システムを構築した経験があれば、サードパーティ製品を理解して変更することがずっと容易になるはずです。
Gitでバージョン管理を行なっているのであれば、このタイミングでユーザーをモデリングするためのトピックブランチを作成しておいてください。
$ git checkout master
$ git checkout -b modeling-users
(最初の行はmasterブランチから作業を始めることを確認するためのものです。そして、modeling-users
トピックブランチはmaster
ブランチを基に作成します。もしすでにmasterブランチにいる場合は、このコマンドを実行する必要はありません)。
6.1 Userモデル
ここから3つの章にわたる最終目標はユーザー登録ページ (図6.1のモックアップ) を作成することですが、今のままでは新しいユーザーの情報を受け取っても保存する場所がないので、いきなりページを作成するわけにはいきません。ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造を作成することです。
Railsでは、データモデルで使用するデフォルトのデータ構造のことをモデルと呼びます (1.2.6にある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.4)。Railsは、本番 (production) アプリケーションですら、データの保存方法の詳細についてほとんど考える必要がないくらいよくできています。
6.1.1データベースの移行
4.4.5で扱ったカスタムビルドクラスのUser
を思い出してください。このクラスは、name
とemail
を属性に持つユーザーオブジェクトでした。このクラスは役に立つ例として提供されましたが、Railsにとって極めて重要な部分である永続性という要素が欠けていました。RailsコンソールでUserクラスのオブジェクトを作っても、コンソールからexitするとそのオブジェクトはすぐに消えてしまいました。この節での目的は、簡単に消えることのないユーザーのモデルを構築することです。
4.4.5のユーザークラスと同様に、name
とemail
の2つの属性からなるユーザーをモデリングするところから始めましょう。後者のemailを一意のユーザー名として使用します3 (パスワードのための属性は6.3で扱います) 。リスト4.9では、以下のようにRubyのattr_accessor
メソッドを使用しました。
class User
attr_accessor :name, :email
.
.
.
end
それとは対照的に、Railsでユーザーをモデリングするときは、属性を明示的に識別する必要がありません。上で簡潔に述べたように、Railsはデータを保存する際にデフォルトでリレーショナルデータベースを使用します。リレーショナルデータベースは、データ行で構成されるテーブルからなり、各行はデータ属性のカラム (列) を持ちます。たとえば、nameとemailを持つユーザーを保存するのであれば、name
とemail
のカラムを持つusers
テーブルを作成します (各行はひとりのユーザーを表します)。カラムをこのように名付けることによって、Active RecordでUserオブジェクトの属性を利用できるようになります。
それでは実際どのように動作するのか見てみましょう (ここまでの説明が抽象的でわかりにくいかもしれませんが、少しだけご辛抱願います。6.1.3から使用しているコンソールの例と、図6.3と図6.6にあるデータベースブラウザのスクリーンショットが理解を助けてくれるでしょう)。リスト5.28で、ユーザーコントローラ (とnew
アクション) を作ったときに使った以下のコマンドを思い出してみてください。
$ rails generate controller Users new --no-test-framework
上のコマンドはコントローラを作成しましたが、同様にモデルを作成するコマンドとして、generate model
があります。name
とemail
の2つの属性を持つUserモデルを作成するコマンドをリスト6.1に示します。
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/[timestamp]_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
(コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習を頭に入れておいてください。コントローラは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と6.3で、マイグレーションを一から手動で作成する方法について説明します)。
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
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に示します。
マイグレーションは、以下のようにrake
コマンド (コラム 2.1) を使って実行することができます。これを“マイグレーションの適用 (migrating up)”と呼びます。
$ bundle exec rake db:migrate
(2.2で、一度このコマンドを実行したことを思い出してみてください) 。初めてdb:migrate
が実行されると、db/development.sqlite3
という名前のファイルが生成されます。これはSQLite5データベースです。db/development.sqlite3
ファイルを開くためのSQLite Database Browserという素晴らしいツールを使って、データベースの構造を詳しく参照することができます (図6.3)。図6.2の表と比べてみてください。図6.3の中に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.2modelファイル
これまで、リスト6.1のUserモデルの作成によってどのように (リスト6.2) のマイグレーションファイルが作成されるかを見てきました。そして図6.3でこのマイグレーションを実行した結果を見ました。development.sqlite3
という名のファイルをusers
テーブルを作成することで更新し、id
、name
、email
、created_at
、updated_at
を作成しました。リスト6.1はモデル自体も作成しました。この節では、以後これらを理解することに専念します。
まず、app/models/
ディレクトリにあるuser.rb
ファイルに書かれたUserモデルのコードを見てみましょう。これは控えめに言ってもとてもよくまとまっています (リスト6.3) (注: もしRails 3.2.2か、それ以前のバージョンを使っている場合はattr_accessible
の行は存在しません。その場合は6.1.2.2で追加する必要があります)。
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
end
4.4.2で行ったことを思い出してみましょう。class User < ActiveRecord::Base
という構文で、User
クラスはActiveRecord::Base
を継承するので、Userモデルは自動的にActiveRecord::Base
クラスのすべての機能を持ちます。もちろん、この継承の知識は、ActiveRecord::Base
に含まれるメソッドなどについて知らなければ何の役にも立ちません。それらの知識の一部についてはこれからご説明します。ただしその前に、完了させておかなければならない作業が2つあります。
モデル注釈
必須というわけではありませんが、annotate
gem (リスト6.4) を使ってRailsのモデルに注釈を追加するようにしておくと便利なことがあります。
Gemfile
にannotate
gemを追加する。 source 'https://rubygems.org'
.
.
.
group :development, :test do
gem 'sqlite3', '1.3.5'
gem 'rspec-rails', '2.11.0'
end
group :development do
gem 'annotate', '2.5.0'
end
group :test do
.
.
.
end
(この注釈機能は本番アプリでは不要なので、annotate
gemはgroup :development
ブロックの中に書きます (group :test
に書いたときと同じ要領です))。次にbundle install
を実行してインストールします。
$ bundle install
これによりannotate
コマンドが使えるようになります。これを実行すると、モデルファイルにデータモデルを含んだコメントが追加されます。
$ bundle exec annotate
Annotated (1): User
実行結果をリスト6.5に示します。
app/models/user.rb
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
attr_accessible :name, :email
end
データモデルをモデルファイルの中にコメントとして残しておくと、モデルにどんな属性があるかを楽に思い出せます。なお簡潔さのため、本書で今後使用するコードにはこの注釈を付けません (もし注釈を最新の状態に保ちたいのであれば、データモデルが変わるたびにannotate
を実行しなければならないことに注意してください)。
アクセス可能な属性
それではもう一度Userモデルに戻りましょう。attr_accessible
の行に注目してください (リスト6.6)。この行は、モデルのどの属性をアクセス可能にするかをRailsに伝えます。たとえば、外部のユーザー (Webブラウザを使用してリクエストを送信するユーザーなど) が変更してもよい属性を指定します。
name
とemail
属性をアクセス可能にする。app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
end
リスト6.6のコードの動作は若干わかりにくいところがあるので注意が必要です。モデルにattr_accessibleを書かない場合は、デフォルトで「モデルのすべての属性がアクセス可能」になります。リスト6.6は、name
とemail
属性が外部のユーザーからアクセス可能であることを指定すると同時に、「 明示的に指定されたname
とemail
属性以外の属性はすべてアクセス不可能とする」ことを暗黙に指定します。この動作を理解することが重要な理由については、第9章で説明します。マスアサインメントの脆弱性から守るためには、attr_accessible
を必ず使用することが重要です。マスアサインメント脆弱性は非常によくあるセキュリティ問題で、多くのRailsアプリケーションで重大なセキュリティホールの原因となりました。
6.1.3ユーザーオブジェクトを作成する
準備が完了しましたので、いよいよActive Recordについて学ぶことにしましょう。先ほど作成したUserモデルを使用します。第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.9のexample_userファイルを明示的にrequireするまでこのオブジェクトにはアクセスできませんでした。しかし、モデルを使うと状況は異なります。4.4.4で見たように、Railsコンソールは起動時にRailsの環境を自動的に読み込み、その環境にはモデルも含まれます。つまり、新しいユーザーオブジェクトを作成するときに余分な作業を行わずに済むということです。
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
コンソールでは上のように、ユーザーオブジェクトはデフォルトの表現として、図6.2とリスト6.5と同じ属性を出力していることがわかります。
User.new
を引数なしで呼んだ場合は、すべての属性がnil
のオブジェクトを返します。4.4.5では、オブジェクトの属性を設定するための初期化ハッシュ (hash) を引数に取るように、Userクラスの例 (example_user.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属性が期待どおり設定されていることがわかります。
開発ログをtailしたまま上を実行していた場合、実行後に新しい行が何も表示されないことに気付いた方もいると思います。これは、User.new
を実行しても単にRubyオブジェクトをメモリ上に作成するだけで、データベースにはアクセスしないためです。このユーザーオブジェクトをデータベースに実際に保存するには、user
変数に対してsave
メソッドを呼びます。
>> user.save
=> true
save
メソッドは、成功すればtrue
を、失敗すればfalse
を返します (現状では、保存はすべて成功するはずです。失敗する場合については6.2で説明します)。 保存すると、SQLコマンドのINSERT INTO "users"
という行が開発ログに追加出力されることがすぐに確認できます。Active Recordによって多数のメソッドが提供されているので、本書では生のSQLを書く必要がありません。従って、本書ではこれ以降はSQLコマンドについての説明を省略します。ただしそれでも、開発ログを監視することによってSQLについて多くのことを学ぶことができるでしょう。
作成した時点でのユーザーオブジェクトは、id
属性、マジックカラムであるcreated_at
属性とupdated_at
属性の値がいずれもnil
であったことを思い出してください。save
メソッドを実行した後に何が変更されたのかを確認してみましょう。
>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
id
には1
という値が代入され、一方でマジックカラムには現在の日時が代入されているのがわかります6。現在、作成と更新のタイムスタンプは同一ですが、6.1.5では異なる値になります。
4.4.5のUserクラスと同様に、Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます7。
>> user.name
Michael Hartl
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Tue, 05 Dec 2011 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:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
User.create
は、true
かfalse
を返す代わりに、ユーザーオブジェクト自身を返すことに注目してください。返されたユーザーオブジェクトは (上の2つ目のコマンドにあるfoo
のように) 変数に代入することもできます。
destroy
はcreate
の逆です。
>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
奇妙なことに、destroy
はcreate
と同じようにそのオブジェクト自身を返しますが、その返り値を使用しても、もう一度destroy
を呼ぶことはできません。そして、おそらくさらに奇妙なことに、destroy
されたオブジェクトは以下のようにまだメモリ上に残っています。
>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
オブジェクトが本当に削除されたかどうかをどのようにして知ればよいでしょうか。そして、保存して削除されていないオブジェクトの場合、どうやってデータベースからユーザーを取得するのでしょうか。いよいよActive Recordでユーザーオブジェクトを検索する方法を学ぶときが来ました。
6.1.4ユーザーオブジェクトを検索する
Active Recordには、オブジェクトを検索するための方法がいくつもあります。これらの機能を使用して、過去に作成した最初のユーザーを探してみましょう。また、3番目のユーザー (foo
) が削除されていることを確認しましょう。まずは存在するユーザーから探してみましょう。
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 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: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
find_by_email
は、users
テーブルのemail
属性に基づいてActive Recordが自動的に生成するメソッドです (ご想像どおり、Active Recordはfind_by_name
というメソッドも自動的に生成します)。 これまでメールアドレスをユーザー名として使用してきたので、このようなfind
は、ユーザーをサイトにログインさせる方法を学ぶときに役に立ちます (第7章)。ユーザー数が膨大になるとfind_by_email
では検索効率が低下するのではないかと心配する方もいるかもしれませんが、焦る必要はありません。この問題およびデータベースのインデックスを使った解決策については6.2.5で扱います。
ユーザーを検索する一般的な方法をあと少しだけご紹介して、この節を終わりにすることにしましょう。まず初めにfirst
メソッドです。
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
読んで字のごとく、first
は単にデータベースの最初のユーザーを返します。次はall
メソッドです。
>> User.all
=> [#<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">,
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">]
ご想像のとおり、all
はデータベースのすべてのユーザーの配列 (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: "2011-12-05 00:57:46", updated_at: "2011-12-05 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"
今ユーザーを更新しました。6.1.3で約束したように、マジックカラムの更新日時が更新されました。
>> user.created_at
=> "2011-12-05 00:57:46"
>> user.updated_at
=> "2011-12-05 01:37:32"
属性を更新するもうひとつの方法は、update_attributes
を使うものです。
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
update_attributes
メソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行います (保存に成功した場合はtrue
を返します)。ただし、attr_accessible
(6.1.2.2) を使って一部の属性のみをアクセス可能にしている場合、このメソッドは期待どおりに動作しません。update_attributes
を使うと、アクセス可能な属性しか更新されなくなるためです。このメソッドを使用していて、モデルの特定のカラムがなぜか更新できなくなったら、それらのカラムがattr_accessible
で指定されているかどうかを確認してください。
6.2ユーザーを検証する
ついに、6.1で作成したUserモデルに、アクセス可能なname
とemail
属性が与えられました。しかし、これらの属性はどんな値でも取ることができてしまいます。現在は (空文字を含む) あらゆる文字列が有効です。名前とメールアドレスには、もう少し何らかの制限があってよいはずです。たとえば、name
は空であってはならず、email
はメールアドレスのフォーマットに従う必要があります。さらに、メールアドレスをユーザーがログインするときの一意のユーザー名として使おうとしているので、メールアドレスがデータベース内で重複することのないようにする必要もあります。
要するに、name
とemail
にあらゆる文字列を許すのは避けるべきです。これらの属性値には、何らかの制約を与える必要があります。Active Recordでは検証 (バリデーション: validation) 機能を使用してそのような制約を与えることができます。ここでは、よく使われるケースのうちのいくつかについて説明します。それらは存在性 (presence)の検証、長さ (length)の検証、フォーマット (format)の検証、一意性 (uniqueness)の検証です。6.3.4では、よく使われる最終検証として確認 (confirmation)を追加します。7.3では、ユーザーが制約に違反したときに、検証機能によって自動的に表示される有用なエラーメッセージをお見せします。
6.2.1最初のユーザーテスト
サンプルアプリケーションの他の機能と同様、Userモデルへの検証の追加もテスト駆動開発 (TDD) で行います。今回はUserモデルを作成したときに
--no-test-framework
(リスト5.28の例とは異なり) 上のフラグを渡さなかったので、リスト6.1のコマンドでは、モデル作成時に、ユーザーをテストするための初期specも同時に生成しています。ただし、生成された初期specは、実質的には空の状態です (リスト6.7)。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
上のコードではpending
メソッドだけが置かれており、何か意味のあるコードでspecを埋めるように促しています。このコードの効果は、Userモデルのspecを実行することで確認できます。
$ bundle exec rspec spec/models/user_spec.rb
*
Finished in 0.01999 seconds
1 example, 0 failures, 1 pending
Pending:
User add some examples to (or delete)
/Users/mhartl/rails_projects/sample_app/spec/models/user_spec.rb
(Not Yet Implemented)
多くのシステムでは、pendingのspecはコマンドライン上で黄色で表示されます。それは、成功 (緑) と失敗 (赤) の間であることを意味します。
デフォルトのspecのアドバイスに従い、リスト6.8に示したいくつかのRSpecの例に置き換えてみましょう。
:name
と:email
属性のテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "user@example.com") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
リスト5.27で見たbefore
ブロックは前処理用で、各サンプルが実行される前にそのブロックの中のコードを実行します。この場合、User.new
と初期化用の有効なハッシュを使って、新しい@user
インスタンス変数を作成します。そして以下のコードは、
subject { @user }
5.3.4でpage
変数を扱ったときと同じように、@user
をテストサンプルのデフォルトのsubjectとして設定します。
リスト6.8の2つのサンプルは、name
属性とemail
属性の存在をテストします。
it { should respond_to(:name) }
it { should respond_to(:email) }
これらのサンプルではRubyのrespond_to?
メソッドを暗黙的に使っています。このメソッドはシンボルを1つ引数として受け取り、引数として与えられたそのシンボルが表すメソッドまたは属性にオブジェクトが応答する場合はtrue
を返し、応答しない場合はfalse
を返します。
$ rails console --sandbox
>> user = User.new
>> user.respond_to?(:name)
=> true
>> user.respond_to?(:foobar)
=> false
(4.2.3でも説明したとおり、Rubyではtrue/falseの真偽値を返すメソッド名の末尾に?記号を置く慣習があることを思い出してください)。これらのテストは、RSpecで使われる論理値の慣習に依存しています。以下のコードは、
@user.respond_to?(:name)
以下のRSpecのコードでテストされます。
@user.should respond_to(:name)
しかし、subject { @user }
があるので、テストの中で@user
を使わずに以下のように書くことができます。
it { should respond_to(:name) }
このようなテストのおかげで、テスト駆動開発をベースに新しい属性やメソッドをUserモデルに追加することができます。さらに、すべてのUser
オブジェクトがこれらのメソッドに応答する必要があるという仕様もここで明らかになりました。
この時点ではテストが失敗することを検証しておく必要があります。
$ bundle exec rspec spec/
6.1.1で、rake db:migrate
を使って開発データベースを作成しましたが、上のテストは失敗します。それというのも、テストデータベースはまだデータモデルの存在を知らないからです (実際、テストデータベースはまだ存在しません)。db:test:prepare
というRakeタスクを使用して、正しい構造のテスト用データベースを作ることができます。これでテストにパスするようになります。
$ bundle exec rake db:test:prepare
上のコマンドは、単に開発データベースのデータモデルdb/development.sqlite3
がテストデータベースdb/test.sqlite3
に反映されるようにするものです。よくある失敗のひとつに、マイグレーションの後でこのRakeタスクが実行できなくなるというのがあります。さらに、テストデータベースはときどき壊れることがあるので、その場合はリセットが必要です。もしテストスイートが理由もなく壊れるようなことがあれば、rake db:test:prepare
を実行して、この問題が解決するか確認してみてください。
6.2.2プレゼンスを検証する
おそらく最も基本的な検証 (validation) はプレゼンス (存在性) です。これは単に、与えられた属性が存在することを検証します。たとえばこの節では、ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証します。7.3.2では、この要求を新しいユーザーを作るためのユーザー登録フォームにまで徹底させる方法を確認します。
最初にname
属性の存在を確認するテストを行いましょう。テスト駆動開発の最初のステップは失敗するテスト (3.2.1) を書くことですが、今回は、適切なテストを書くための検証事項についてまだ十分に理解していないので、まず最初に検証を書きます。検証については、コンソールを使って理解することにします。次に、検証部分をコメントアウトし、失敗するテストを書いて、最後にコメントアウトを解除した検証でテストがパスするかどうかを確認します。この手続きは、このような単純なテストでは大げさで気取ったものに感じられるかもしれませんが、著者はこれまでにテストそのものが間違っている “単純な” テストを山ほど見てきました。テスト駆動開発を慎重に進めることは、テストが正しく進められているという自信を得る唯一の方法です (上で紹介したコメントアウトのテクニックは、コードはあってもテストがどこにもないようなひどいアプリケーションを急いで救出するときにも役に立ちます)。
name属性の存在を検査する方法は、リスト6.9に示したとおり、validates
メソッドにpresence: true
という引数を与えて使うことです。presence: true
という引数は、要素がひとつのオプションハッシュです。4.3.4のようにメソッドの最後の引数としてハッシュを渡す場合、波括弧を付けなくても問題ありません (5.1.1でも説明したように、Railsのオプションハッシュは繰り返し登場するテーマです)。
name
属性の存在を検証する。app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true
end
リスト6.9は一見魔法のように見えるかもしれません。しかしattr_accessible
同様、validates
もただのメソッドに過ぎません。括弧を使用してリスト6.9を同等のコードに書き換えたものを以下に示します。
class User < ActiveRecord::Base
attr_accessible(:name, :email)
validates(:name, presence: true)
end
コンソールを起動して、Userモデルに検証を追加した効果を見てみましょう9。
$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.save
=> false
>> user.valid?
=> false
user.save
はfalse
を返しました。これは保存に失敗したことを意味します。最後のコマンドは、valid?
メソッドで、オブジェクトがひとつ以上の検証に失敗したときにfalse
を返します。 すべての検証がパスした場合はtrue
を返します。今回の場合、検証がひとつしかないので、どの検証が失敗したかわかります。しかし、失敗したときに作られるerrors
オブジェクトを使って確認すれば、さらに便利です。
>> user.errors.full_messages
=> ["Name can't be blank"]
(このエラーメッセージから、Railsが属性の存在性を検査するときにblank?
メソッド (4.4.3の終わりに登場) を使用していることが伺えます。)
次は失敗するテストです。最初に、テストが失敗することを確認するために、この時点 (リスト6.10) で検証をコメントアウトしてみましょう。
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
# validates :name, presence: true
end
最初の段階の検証テストをリスト6.11に示します。
name
属性の検証に対する、失敗するテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
end
最初の新しいサンプルは、基本となるただのテストです。これを使用して、まず@user
オブジェクトが有効かどうかを確認します。
it { should be_valid }
上のコードは6.2.1で見た別のRSpecの真偽値の慣習を示すサンプルです。あるオブジェクトが、真偽値を返すfoo?
というメソッドに応答するのであれば、それに対応するbe_foo
というテストメソッドが (自動的に) 存在します。この場合、以下のメソッド呼び出しの結果をテストすることができます。
@user.valid?
上のコードの場合、対応するテストコードは以下のようになります。
@user.should be_valid
以前と同じように、subject { @user }
があるので@user
を使わずに書くことができます。
it { should be_valid }
2つ目のテストは、まずユーザーのnameに無効な値 (blank) を設定し、@user
オブジェクトの結果も無効になることをテストして確認します。
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
ユーザーのnameに無効な値 (blank) を設定するにはbefore
ブロックを使います。次にユーザーオブジェクトの結果が無効であることを確認します。
この時点でテストが失敗することを確認してください。
$ bundle exec rspec spec/models/user_spec.rb
...F
4 examples, 1 failure
それではここで、テストにパスするために検証部分のコメントアウトを解除しましょう (つまり、リスト6.10をリスト6.9に戻します)。
$ bundle exec rspec spec/models/user_spec.rb
....
4 examples, 0 failures
もちろん、今度はメールアドレスの存在性も検証しましょう。このテスト (リスト6.12) は、name
属性のテストと似ています。
email
属性の存在性のテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email is not present" do
before { @user.email = " " }
it { should_not be_valid }
end
end
以下のリスト6.13に示すように、メールアドレス検証の実装も名前の検証と実質的に同じです。
name
属性とemail
属性の存在性を検証する。app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true
validates :email, presence: true
end
これですべてのテストにパスするはずです。これで、存在性の検証は完成しました。
6.2.3長さを検証する
各ユーザーは、Userモデル上に名前を持つよう強制されます。しかし、これだけでは十分ではありません。ユーザーの名前はサンプルWebサイトに表示されるものなので、名前の長さにも制限を与える必要があります。6.2.2で既に同じような作業を行ったので、この実装は簡単です。
まずはテストを作成します。最長のユーザー名の長さに科学的な根拠はありませんので、単に50
という上限として手頃な値を使うことにします。つまりここでは、51
文字の名前は長すぎることを検証します (リスト6.14)。
name
の長さ検証のテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when name is too long" do
before { @user.name = "a" * 51 }
it { should_not be_valid }
end
end
リスト6.14では、51文字の文字列を簡単に作るために “文字列のかけ算” を使いました。結果をコンソール上で確認できます。
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
今の時点ではリスト6.14のテストは失敗するはずです。これをパスさせるためには、長さを強制するための検証の引数について知っておく必要があります。:maximum
パラメータと共に用いられる:length
は、長さの上限を強制します (リスト6.15)。
name
属性の長さの検証を追加する。app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true
end
これでテストにパスするはずです。パスしたテストスイートを流用して、今度は少し難しい、メールアドレスのフォーマット検証作業に取りかかりましょう。
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
メソッドを使ってメールアドレス
の配列の各要素を繰り返し取り出しました (4.3.2)。このテクニックを学んだことで、基本となるメールアドレスフォーマット検証のテストを書く準備が整いました (リスト6.16)。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email format is invalid" do
it "should be invalid" do
addresses = %w[user@foo,com user_at_foo.org example.user@foo.
foo@bar_baz.com foo@bar+baz.com]
addresses.each do |invalid_address|
@user.email = invalid_address
@user.should_not be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
addresses.each do |valid_address|
@user.email = valid_address
@user.should be_valid
end
end
end
end
既に述べたとおり、上のテストはすべてを盛り込んだものではありませんが、一般的に有効なメールアドレスの形式であるuser@foo.COM
、THE_US-ER@foo.bar.org
(大文字、アンダースコア、複合ドメイン) 、first.last@foo.jp
(一般的な企業のユーザー名「名.姓
」と、2文字のトップレベルドメイン「jp
」) を、いくつかの無効な形式と共に確認します。
メールアドレスのフォーマット検証を行うアプリケーションコードでは、validates
メソッドの:format
引数に、フォーマットを定義するための正規表現 (regular expression) (またはregex) を与えます (リスト6.17)。
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, 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, format: { with: VALID_EMAIL_REGEX }
このパターンに一致するメールアドレスだけが有効であることをチェックします (VALID_EMAIL_REGEX
は大文字で始まるので、Rubyの定数として扱われ、値は変更できません)。
ところで、この正規表現パターンはどうやって作ればよいのでしょうか。正規表現は簡潔な (読めないという人もいるにはいますが) テキストパターンマッチング言語から成ります。正規表現を組み立てることを学ぶのはそれだけでひとつの技術分野であり、手短に説明するのは簡単ではありませんが、ともあれ最初の説明のためにVALID_EMAIL_REGEX
をビットサイズの破片に分解しました (表6.1)10。限られた紙面で正規表現を本格的に学ぶには、素晴らしい正規表現エディタであるRubular (図6.4) が必要不可欠です11。RubularのWebサイトは、正規表現を作るための美しく対話的なインターフェイスを持っています。また、手軽な正規表現のクイックリファレンスにもなります。Rubularのサイトをブラウザで開き、表6.1の表現をひとつずつ入力して結果を確かめながら正規表現を学ぶことをぜひともお勧めします。正規表現について学んだことがなくても、Rubularを使えば、2〜3時間ほどで慣れることができます (注: リスト6.17の正規表現をRubularで使う場合、冒頭の\Aと末尾の\zの文字は含めないでください)。
表現 | 意味 |
---|---|
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i | (完全な正規表現) |
/ | 正規表現の開始を示す |
\A | 文字列の先頭 |
[\w+\-.]+ | 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す |
@ | アットマーク |
[a-z\d\-.]+ | 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す |
\. | ドット |
[a-z]+ | 英小文字を少なくとも1文字以上繰り返す |
\z | 文字列の末尾 |
/ | 正規表現の終わりを示す |
i | 大文字小文字を無視するオプション |
ところで、公式標準によるとメールアドレスに完全に一致する正規表現は存在するのだそうです。しかし、苦労して導入するほどの甲斐はありません。リスト6.17の例は問題なく動作しますし、おそらく公式のものより良いでしょう12。
これでテストはすべてパスするはずです(実際、この有効なメールアドレスのテストはこれまでいつもパスしてきました。正規表現のプログラミングは間違いが起こりやすいことで有名なので、ここで行なっている有効なメールアドレスのテストは、主としてVALID_EMAIL_REGEX
に対する形式的な健全性チェックに過ぎません)。残る制約は、メールアドレスが一意であることを強制するものだけとなりました。
6.2.5一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validates
メソッドの:unique
オプションを使います。ただしここで重大な警告があります。以下の文面は流し読みせず、必ず注意深く読んでください。
今回もいつものようにテストを作成するところから始めます。モデルのテストではこれまで、主にUser.new
を使ってきました。このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります13。(最初の段階の) 重複メールアドレスのテストをリスト6.18に示します。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.save
end
it { should_not be_valid }
end
end
上のコードでは、@user
と同じメールアドレスのユーザーを事前に作成してテストする手法が使われています。このとき、同じ属性のユーザーを作るために、@user.dup
メソッドが使われています。同じ属性のユーザーが保存された後では、元の@user
と同じメールアドレスが既にデータベース内に存在しているため、@user
は無効になります。
先ほどのリスト6.18のテストは、リスト6.19のコードを用いてパスさせることができます。
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
実装の途中ですが、ここでひとつ補足します。通常、メールアドレスでは大文字小文字が区別されません。すなわち、foo@bar.com
はFOO@BAR.COM
やFoO@BAr.coM
と書いても扱いは同じです。従って、メールアドレスの検証ではこのような場合も考慮する必要があります14 。リスト6.20のコードでは、大文字小文字を区別しないものをテストしています。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
.
.
.
describe "when email address is already taken" do
before do
user_with_same_email = @user.dup
user_with_same_email.email = @user.email.upcase
user_with_same_email.save
end
it { should_not be_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"
>> user_with_same_email = user.dup
>> user_with_same_email.email = user.email.upcase
>> user_with_same_email.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、user_with_same_email.valid?
はtrue
になります。しかし、ここではfalse
になる必要があります。幸い、:uniqueness
では:case_sensitive
といううってつけのオプションが使用できます (リスト6.21)。
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
上のコードでは、単にtrue
をcase_sensitive: false
で置き換えただけであることに注目してください。Railsはこの場合、:uniqueness
をtrue
と判断します。この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。
一意性の警告
上で示した警告には、1つ小さな問題があります。
validates :uniqueness
を使用しても、一意性は保証されません。
えっ!何が問題になるのでしょうか。以下のシナリオを見てください。
- アリスはサンプルアプリケーションにユーザー登録します。メールアドレスはalice@wonderland.comです。
- アリスは誤って “Submit” を素早く2回クリックしてしまいます。そのためリクエストが2つ連続で送信されます。
- 次のようなことが順に発生します。リクエスト1は、検証にパスするユーザーをメモリー上に作成します。リクエスト2でも同じことが起きます。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存されます。
- この結果、一意性の検証が行われているにもかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。
上のシナリオが信じがたいもののように思えるかもしれませんが、どうか信じてください。RailsのWebサイトでは、トラフィックが多いときにこのような問題が発生する可能性があるのです。幸い、解決策の実装は簡単です。実は、データベースレベルでも一意性を強制するだけでこの問題は解決します。具体的には、emailカラムにデータベースのインデックスを作成し、そのインデックスが一意であることを要求します。
emailインデックスを追加すると、データモデリングの変更が必要になります。Railsでは (6.1.1で見たように) マイグレーションでインデックスを追加します。6.1.1で、Userモデルを生成すると自動的に新しいマイグレーションが作成されたことを思い出してください (リスト6.2)。今回の場合は、既に存在するモデルに構造を追加するので、以下のようにmigration
ジェネレーターを使用してマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。リスト6.22のように定義を記述する必要があります15。
db/migrate/[timestamp]_add_index_to_users_email.rb
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/schema.rb
を開いてみると以下のような行があるはずです。
add_index "users", ["email"], :name => "index_users_on_email", :unique => true
残念なことに、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。それは、メールアドレスをデータベースに保存する前にすべての文字を小文字に変換することです。その理由は、データベースのアダプタが常に大文字小文字を区別するインデックスを使っているとは限らないからです16。これを行うにはコールバックというテクニックを利用します。コールバックとは、Active Recordオブジェクトが持続している間のどこかの時点で、Active Recordオブジェクトに呼び出してもらうメソッドです (詳しくはRailsガイドの「Active Recordコールバック」を参照してください)。今回の場合は、before_save
コールバックを使います。リスト6.23に示したように、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します。
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
before_save { |user| user.email = email.downcase }
.
.
.
end
リスト6.23のコードは、before_save
コールバックにブロックを渡してユーザーのメールアドレスを設定します。設定されるメールアドレスは、現在の値をStringクラスのdowncase
メソッドを使って小文字バージョンにしたものです。このコードは少し上級者向けなので、今はただ、このコードが動作することを信じてください。それでは気の済まない方は、リスト6.19から一意性の検証部分をコメントアウトし、重複したメールアドレスを持つユーザーを試しに作成してみれば、エラーが発生するはずです (このテクニックについては8.2.1でもう一度取り上げます)。
これで、先に述べたアリスのシナリオはうまくいくようになります。データベースは、ユーザーのレコードを最初のリクエストに基づいて保存しますが、次の保存は一意性の制約に反するので拒否します (Railsのログにエラーが出力されますが、害は生じません。ここで発生したActiveRecord::StatementInvalid
例外を実際にキャッチすることもできます。具体例についてはInsoshiのコードを見てください。ただしこのチュートリアルでは解説しません)。インデックスをemail属性に追加したことで、6.1.4で述べた2番目の目標も達成されます。これは、find_by_email
の効率の問題がインデックスによって解決されたためです (コラム 6.2)。
データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要です。たとえば、リスト6.2のマイグレーションによって作成されたemail属性について考えてみましょう。第7章ではユーザーをサンプルアプリにログインできるようにしますが、このとき、送信されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探しだす必要があります。残念なことに、(インデックスなどの機能を持たない) 素朴なデータモデルにおいてユーザーをメールアドレスで探すには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と与えられたメールアドレスを比較するという非効率的な方法しかありません。これは、データベースの世界では全表スキャンとして知られており、数千のユーザーがいる実際のサイトでは極めて不都合です。
emailカラムにインデックスを追加することで、この問題を解決することができます。データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。本の中で、与えられた言葉 (例えば、“foobar”) が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります。本の索引を利用すれば、“foobar”を含むすべてのページを索引の中から探すだけで済みます。データベースのインデックスも本質的には本の索引と同じように動作します。
6.3セキュアなパスワードを追加する
この節では、ユーザーに最後の属性を追加します。セキュアパスワードは、サンプルアプリケーションでユーザーを認証するために使用します。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) 暗号化したものをデータベースに保存します。また、入力されたパスワードを使用してユーザーを認証する手段と、第8章で使用する、ユーザーがサイトにサインインできるようにする手段も提供します。
ユーザーの認証は、パスワードの送信、暗号化、データベース内の暗号化された値との比較という手順を踏みます。比較の結果が一致すれば、送信されたパスワードは正しいと認識され、そのユーザーは認証されます。ここで、生のパスワードではなく、暗号化されたパスワード同士を比較していることに注目してください。こうすることで、生のパスワードをデータベースに保存するという危険なことをしなくてもユーザーを認証できます。これで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれます。
セキュアなパスワードの実装は、has_secure_password
というRailsのメソッドを呼び出すだけでほとんど終わってしまいます (このメソッドはRails 3.1から導入されました)。このメソッド1つだけでセキュアなパスワードの実装がほとんど終わってしまうので、逆にこの機能を一から手作りするのは簡単ではありません。そのため、6.3.2からは多数のテストを作成し、これらをすべてパスするようにします。この機能の実装中に泥沼にはまり込んでしまうこともあると思いますが、どうかあきらめずに最後までやり通してください。6.3.4までたどり着ければ、その苦労を補って余りある褒美が待っています (スクリーンキャストは、このような一からの手作り開発手順を解説するのに向いています。この課題を十分に理解したい方は「Railsスクリーンキャスト (日本語版あり)」を参照してください)。
6.3.1暗号化されたパスワード
最初に、ユーザーのデータモデルに必要な変更を行います。具体的には、users
テーブルにpassword_digest
カラムを追加します (図6.5)。なお、digestという言葉は暗号学的ハッシュ関数が用語の語源です。6.3.4の実装が動作するには、カラム名を正確にpassword_digest
とする必要があります。パスワードを適切に暗号化することで、たとえ攻撃者によってデータベースからパスワードをコピーされてもWebサイトにサインインされることのないようにできます。
ハッシュ関数には最新のbcryptを使用し、パスワードを不可逆的に暗号化してパスワードハッシュを作成します。サンプルアプリケーションでbcryptを使用するために、bcrypt-ruby
gemをGemfile
に追加します (リスト6.24)。
bcrypt-ruby
をGemfile
に追加する。 source 'https://rubygems.org'
gem 'rails', '3.2.14'
gem 'bootstrap-sass', '2.1'
gem 'bcrypt-ruby', '3.0.1'
.
.
.
次にbundle install
を実行します。
$ bundle install
このとき、システム環境によっては以下の警告が出力されることがあります。
make: /usr/bin/gcc-4.2: No such file or directory
この問題を修正するには、clang
フラグを追加してRVMを再インストールします。
$ rvm reinstall 1.9.3 --with-gcc=clang
ユーザーはpassword_digestカラムにアクセスしなければならないので、リスト6.25に示したように、ユーザーオブジェクトはpassword_digest
に応答する必要があります。
password_digest
カラムがあることを確認するテスト。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
.
.
.
end
このテストがパスするには、最初にpassword_digest
カラム用の適切なマイグレーションを生成します。
$ rails generate migration add_password_digest_to_users password_digest:string
上のコマンドの最初の引数はマイグレーション名、次の引数は作成する属性の名前と型です (リスト6.1で最初にusers
テーブルを生成したときのマイグレーションと比較してみてください)。マイグレーション名は自由に指定できますが、上のように末尾を_to_users
にしておくことをお勧めします。こうしておくと、users
テーブルにカラムを追加するマイグレーションがRailsによって自動的に作成されるからです。また、上のコマンドに2番目の引数を与えることで、リスト6.26のように完全なマイグレーションを構成するための情報をRailsに与えることができます。
password_digest
カラムをusers
テーブルに追加するマイグレーション。db/migrate/[ts]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
上のコードでは、add_column
メソッドを使用してpassword_digest
カラムをusers
テーブルに追加しています。
以下のように開発データベースをマイグレーションしてテストデータベースを準備することで、リスト6.25の失敗するテストをパスすることができます。
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
$ bundle exec rspec spec/
6.3.2パスワードと確認
図6.1のモックアップに示したように、ユーザーにパスワードを確認させるようにしましょう。パスワードの確認入力は、入力ミスを減らすためにWebで広く使用されています。パスワード確認の強制はコントローラの階層でも行うことができますが、モデルの中でActive Recordを使用して制限を与えるのが慣習になっています。そのためには、password
属性とpassword_confirmation
属性をUserモデルに追加し、レコードをデータベースに保存する前に2つの属性が一致するように要求します。これまでに使用した属性と異なり、パスワード関連の属性は仮想にします。つまり、これらの属性は一時的にメモリ上に置き、データベースには保存されないようにします。6.3.4でも説明しますが、これらの仮想属性はhas_secure_password
では自動的に実装されます。
最初に、respond_to
を使用してパスワードとパスワードの確認をリスト6.27のようにテストします。
password
属性とpassword_confirmation
属性をテストする。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
end
上のコードでは、以下のようにUser.new
ハッシュの初期化に:password
と:password_confirmation
を追加していることに注目してください。
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
パスワードは空欄であってはならないので、パスワードの存在確認テストを別に追加します。
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
パスワードの不一致テストはこのすぐ後に追加するので、上のコードではパスワードとパスワードの確認を両方とも空欄にすることでパスワードの存在確認テストを行なっています。ここでは、1つの行でいくつもの代入を行えるRubyの機能を使用しています。たとえば、以下のようにコンソール上でa
とb
に3
を一括で代入することができます。
>> a = b = 3
>> a
=> 3
>> b
=> 3
この機能を使用して、ここでは以下のように2つのパスワード関連の属性をどちらも" "
に設定しています。
@user.password = @user.password_confirmation = " "
パスワードとパスワードの確認が一致するかどうかもテストする必要があります。パスワードが一致する場合については既にit { should be_valid }
で確認できるので、次は以下のように不一致の場合のテストを追加します。
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
原理的にはテストは以上で終わりですが、しかしまだカバーされていない状態があります。パスワードの確認が空欄の場合はどうなるでしょうか。「パスワードの確認がスペース文字だがパスワードは有効」な場合、両者は一致しないのでパスワードの確認の検証で検出されます。「パスワードとパスワードの確認がどちらもスペース文字」の場合、パスワードの存在確認テストで検出されます。残念ながら、1つだけ漏れがあります。それは、パスワードの確認がnilの場合です。この状態はWebからの入力では決して発生しませんが、コンソールでなら以下のように発生させることができます。
$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?> password: "foobar", password_confirmation: nil)
パスワードの確認がnil
の場合、パスワードの確認の検証はRailsによって実行されません。つまり、コンソール上でならパスワードの確認を回避してユーザーを作成できてしまうということです (もちろん、現時点では検証をそもそも実装していないので、実際には上のコードはすべて動作してしまいます) 。この状態を防ぐには、nilを検出できるテストを追加します。
describe "when password confirmation is nil" do
before { @user.password_confirmation = nil }
it { should_not be_valid }
end
(この振る舞いは、Railsのマイナーなバグであると思います。おそらく今後のバージョンで修正されるでしょう。いずれの場合も、nilの検証を加えることで問題は生じません)。
ここまでのすべてを盛り込んだ失敗するテストをリスト6.28に示します。この節の冒頭で述べたように、has_secure_password
には多くの機能が含まれているため、セキュアパスワードを手作りで実装するのは簡単ではありません。従って、この時点では新しいテストはすべて失敗します。6.3.4でこれらのテストがパスするようにします。
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
describe "when password confirmation is nil" do
before { @user.password_confirmation = nil }
it { should_not be_valid }
end
end
6.3.3ユーザー認証
パスワード機構というパズルの最後のひとかけらは、ユーザーをメールアドレスとパスワードに基いて取得する手段です。この作業は2つに分けるのが自然です。最初に、ユーザーをメールアドレスで検索します。次に、受け取ったパスワードでユーザーを認証します。
最初の手順の実装は簡単です。6.1.4でも説明したように、find_by_email
メソッドを使用すれば、受け取ったメールアドレスでユーザーを検索できます。
user = User.find_by_email(email)
次の手順は、authenticate
メソッドを使用して、受け取ったパスワードがユーザーのパスワードと一致することを確認します。第8章では、以下のようなコードを使用して現在の (サインインしている) ユーザーを取得する予定です。
current_user = user.authenticate(password)
受け取ったパスワードがユーザーのパスワードと一致するとユーザーが返され、一致しない場合はfalse
が返されます。
これまで同様、RSpecを使用してauthenticate
メソッドへの要求内容を表現することができます。ただし、このテストはこれまでよりも高度な内容になるため、いくつかに分割して説明します。RSpecが初めての方は、この節を繰り返し読んでみてください。最初に、Userオブジェクトがauthenticate
に応答することを要求します。
it { should respond_to(:authenticate) }
次に、パスワードが一致する場合と一致しない場合についてそれぞれ記述します。
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by_email(@user.email) }
describe "with valid password" do
it { should == found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
end
上のコードで、before
ブロックはユーザーをデータベースに事前に保存します。これにより、find_by_email
メソッドが動作するようになります。このメソッドをlet
メソッドで以下のようにテストします。
let(:found_user) { User.find_by_email(@user.email) }
これまでいくつかの演習でlet
メソッドを使用してきましたが、今回のようにチュートリアルの本文で言及するのはこれが初めてです。let
メソッドの詳細についてはコラム 6.3を参照してください。
2つのdescribe
ブロックでは、@user
とfound_user
が一致する場合と一致しない場合についてそれぞれテストします。コードで使用されている二重等号の演算子==
は、オブジェクト同士が同値であるかどうかを調べます(4.3.1)。以下のコードに注目してください。
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
上のコードではlet
がもう一度使用されており、さらにspecify
というメソッドも使用されています。実は、このspecifyはit
と同義であり、it
を使用すると英語として不自然な場合にこれで代用することができます。この場合、「it should not equal wrong user」(itはユーザーなど) とするのは英語として自然ですが、「user: user with invalid password should be false」は不自然であり、「specify: user with invalid password should be false」とすれば自然になります。
RSpecのletメソッドを使用すると、テスト内で簡単にローカル変数を作成することができます。文法は一見奇妙ですが、動作は変数への割り当てと似ています。letの引数はシンボルであり、さらにブロックを引数に取ります。そのブロックは、このシンボル名を持つローカル変数に値を返します。つまり、以下のコードを実行すると、
let(:found_user) { User.find_by_email(@user.email) }
found_userという変数が作成され、その値はfind_by_emailの返り値に等しくなります。これにより、この変数はテスト中すべてのbeforeまたはitブロックで利用できるようになります。letでは値がメモ化 (memoize) されるという特長があり、ある呼び出しから次の呼び出しに渡って値を利用できます (メモ化は技術用語であり、決して "memorize" の誤りではありません) 。この場合、found_user変数はletによってメモ化され、find_by_emailが実際に呼び出されるのはUserモデルのspecが実際に実行されるときだけとなります。
最後に、セキュリティの常道として、パスワードの長さ検証をテストします。以下のコードでは、パスワードは6文字以上であることを要求します。
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
ここまでのテストをすべて集約したものをリスト6.29に示します。
authenticate
メソッドをテストする。spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:authenticate) }
.
.
.
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by_email(@user.email) }
describe "with valid password" do
it { should == found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
end
end
コラム 6.3で解説したようにlet
では値がメモ化されます。これにより、リスト6.29にある、最初のネストしているdescribe
ブロックはlet
でfind_by_email
を使用してデータベースからユーザーを取得します。しかしその次のネストしているdescribe
ブロックは (メモ化された値を利用するので) データベースにアクセスしません。
6.3.4ユーザーがセキュアなパスワードを持っている
認証システムを最初からフル作成していたRails 3.0向けRailsチュートリアル17を参照いただくとわかるように、以前のバージョンのRailsでは、セキュアパスワードの実装は面倒で時間のかかる作業でした。しかし今では、Web開発者が認証システムというものを以前よりも深く理解するようになり、最新のRailsには認証システムも同梱されるようになりました。ここまで実装を進めてきたので、あとほんの数行を追加してセキュアパスワードの実装を完了し、テストスイートを緑色 (成功) にしましょう。
最初に、password
カラムとpassword_confirmation
カラムをアクセス可能にし (6.1.2.2)、新しいユーザーを初期化ハッシュでインスタンス化できるようにします。
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
リスト6.6のモデルに従い、適切なシンボルをアクセス可能な属性のリストに以下のように追加します。
attr_accessible :name, :email, :password, :password_confirmation
次に、パスワードの存在確認と長さチェックが必要です。後者の実装には、6.15の:maximum
キーに似た:minimum
キーを使用します。
validates :password, presence: true, length: { minimum: 6 }
次に、password
属性とpassword_confirmation
属性を追加し、パスワードが存在することを要求し、パスワードとパスワードの確認が一致することを要求し、さらにauthenticate
メソッドを使用して、暗号化されたパスワードとpassword_digest
を比較してユーザーを認証するという多くの手順が必要です。この実装が唯一手間のかかる箇所ですが、最新のRailsではhas_secure_password
を使用するだけでこれらの機能をすべて自由に利用できます。
has_secure_password
データベースにpassword_digest
カラムを置くという条件さえ守れば、上のメソッドをモデルに追加するだけで新規ユーザーの作成と認証をセキュアにすることができます。
(has_secure_password
の実装に興味のある方は、secure_password.rbのソースコードを参照してみるとよいでしょう。このソースコードには十分な解説があり、しかも読みやすくできています。そのコードに、以下の行があることに注目してください。
validates_confirmation_of :password
上のコードを実行するだけで、(Rails APIに記載されているように) password_confirmation
という属性が作成されます。このコードにはpassword_digest
属性の検証も含まれますが、第7章ではこれがいいことばかりとは限らないことをお見せします)
最後に、パスワードの確認について存在チェックを行います。
validates :password_confirmation, presence: true
これらの3つの要素をすべて反映したUserモデルをリスト6.30に示します。これでセキュアパスワードの実装は完了です。
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_secure_password
before_save { |user| user.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,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, presence: true, length: { minimum: 6 }
validates :password_confirmation, presence: true
end
ここまで来たら、テストスイートがパスすることを確認しましょう。
$ bundle exec rspec spec/
6.3.5ユーザーを作成する
以上でUserモデルの基本部分が完了しましたので、今度は7.1でユーザー情報表示ページを作成するときに備えて、データベースにユーザーを1人新規で作成しましょう。この作業によって、これまでの節で行なってきた実装が動作することも実感できることでしょう。テストスイートがパスするだけでは味気ないので、実際に開発データベースにユーザーを登録することで喜びを感じていただければと思います。
ただしWebからのユーザー登録はまだできない (完成は第7章です) ので、Railsコンソールを使用して手動でユーザーを作成することにしましょう。6.1.3のときとは異なり、今回はサンドボックスで起動する必要はありません。データベースに実際にレコードを保存しなければ意味がないからです。
$ 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: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJ...">
実際にデータが登録されたことを確認するために、SQLite Database Browserを使用して開発データベース (db/development.sqlite3
) の中にある行を見てみましょう (図6.6)。データモデルの属性に対応するカラムは図6.5で定義されていることを思い出してください。
コンソールに戻ってpassword_digest
属性を参照してみると、リスト6.30のhas_secure_password
の効果を確認できます。
>> user = User.find_by_email("mhartl@example.com")
>> user.password_digest
=> "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21yZIMSH/xoy"
上の文字列は、パスワード ("foobar"
) を暗号化したものであり、ユーザーオブジェクトを初期化するのに使用されました。また、最初に無効なパスワード、次に有効なパスワードを与えることでauthenticate
の動作を確認することもできます。
>> user.authenticate("invalid")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJ...">
要求に沿って、authenticate
メソッドはパスワードが無効なときはfalse
を返し。パスワードが有効なときはユーザー自身を返しています。
6.4最後に
この章では、まったく最初からUserモデルを作成し、それにname
属性とemail
属性を与え、さまざまなパスワード属性も与え、値を制限する多くの重要な検証も追加しました。さらに、与えられたパスワードをセキュアに認証できるようにしました。以前のバージョンのRailsであれば、このような実装を行うためのコードは現在の倍以上になっていたことでしょう。現在はコンパクトなvalidates
メソッドやhas_secure_password
メソッドがあるおかげで、わずか10行程度のソースコードでUserモデルを完成させることができました。
次の第7章では、ユーザーを作成するためのユーザー登録フォームを作成し、各ユーザーの情報を表示するためのページも作成します。第8章では、6.3の認証システムを利用して、ユーザーが実際にWebサイトにサインインできるようにします。
Gitを使用している方は、しばらくコミットしていなかったのであれば、この時点でコミットしておくのがよいでしょう。
$ git add .
$ git commit -m "Make a basic User model (including secure passwords)"
次にmasterブランチにマージバックします。
$ git checkout master
$ git merge modeling-users
6.5演習
- リスト6.23の、メールアドレスを小文字に変換するコードに対するテストを、リスト6.31のように作成してください。
before_save
の行をコメントアウトすることで、リスト6.31が正しい対象をテストしていることを確認してください。 before_save
コールバックをリスト6.32のように書いてもよいことを、テストスイートを実行して確認してください。- Rails APIサイトの
ActiveRecord::Base
の項を読み通し、どんなことができるかを把握してください。 - Rails APIサイトで
validates
メソッドを調べ、どんなことができるか、どんなオプションがあるかを調べてください。 - Rubularで2〜3時間ほど遊んでみてください。
require 'spec_helper'
describe User do
.
.
.
describe "email address with mixed case" do
let(:mixed_case_email) { "Foo@ExAMPle.CoM" }
it "should be saved as all lower-case" do
@user.email = mixed_case_email
@user.save
@user.reload.email.should == mixed_case_email.downcase
end
end
.
.
.
end
before_save
コールバックの別の実装。app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_secure_password
before_save { email.downcase! }
.
.
.
end
- この名前の由来は 'active record pattern' です。Martin Fowler著「エンタープライズ アプリケーションアーキテクチャパターン 」で特定および命名されました。↑
- 「エスキューエル」と発音しますが、「スィークゥエル」もよく使われます。↑
- メールアドレスをユーザー名にしたことで、理屈の上では将来ユーザー同士で通信できるように拡張できる可能性が開かれます。↑
t
オブジェクトが具体的に何をしているのかを正確に知る必要はありませんので、どうか心配しないでください。抽象化レイヤの素晴らしい点は、それが何であるかを知る必要がないという点です。安心してt
オブジェクトに仕事を任せればよいのです。↑- 公式には「エスキューエライト (ess-cue-ell-ite)」と発音しますが、本来は誤りとされている「スィークゥエライト (sequel-ite)」もよく使われています。↑
"2011-12-05 00:57:46"
というタイムスタンプが気になった方もいると思いますが、著者はこの箇所を真夜中過ぎに書いたわけではありません。実はこのタイムスタンプは協定世界時 (UTC) に合わせてあります。これはグリニッジ標準時 (GMT) と同様、標準時間として使用されます。「NIST時刻と周波数FAQ」によると、問: 協定世界時 (Coordinated Universal Time) の略称がCUTではなくUTCなのはなぜですか。答え: 協定世界時システムは、1970年に国際電気通信連合 (ITU) の技術専門家の国際諮問グループによって考案されました。このときITUは、混乱を最小限にとどめるために、略称を1つだけにしたいと考えました。このとき、英語式のCUTもフランス式のTUCも満場一致とならず、両者の妥協案としてUTCという略語が採用されました。↑user.updated_at
の値に注目してください。上の脚注にも書いたとおり、このタイムスタンプはUTCベースです。↑- 例外と例外ハンドリングは、ある意味でRubyの高度なテーマです。本書では例外についてこれ以上言及しません。しかし例外が重要なものであることも確かなので、1.1.1で推薦したRuby本で例外について詳しく学ぶことをお勧めします。↑
- 今後、コンソールコマンドの出力は、特に教育的効果が高いと思える場合 (ここでの
User.new
の場合など) を除いて省略いたします。↑ - 表6.1の正規表現の説明における「文字」は、実は「小文字のみ」が対象になっていることに注意してください。ただし、正規表現の末尾に
i
オプションを追加してあるので、大文字小文字が区別されずにマッチするようになっています。↑ - 著者同様このツールを便利だと思ってくださる方は、Michael Lovittの素晴らしい成果に報いるために、どうかRubularへの寄付をお願いいたします。↑
- 驚いたことに、公式標準によると
"Michael Hartl"@example.com
のようなクォートとスペースを使用したメールアドレスも有効なのだそうです。まったく馬鹿げています。今あなたが使用しているメールアドレスが、英文字、数字、アンダースコア、ドット以外の文字を含んでいるのであれば、そういう文字を含まないメールアドレスに変えることをお勧めします。ただしここで注意すべきことがあります。リスト6.17の正規表現では、プラス記号の使用を許していることに注目してください。これは、Gmail (他のメールサービスにもあるかもしれません) ではプラス記号を使用すると便利なことがあるためです。Gmailでは、example.comからのメールをフィルタするためにusername+example@gmail.comを使用することができます。こうすると、Gmailのusername@gmail.comというアドレスに転送され、exampleという文字をフィルタできるようになります。↑ - この節の冒頭で簡単に紹介したように、この目的に使用できる専用のテストデータベース
db/test.sqlite3
があります。↑ - 技術的には、メールアドレスのうちドメイン名部分だけが (本当は) 大文字小文字を区別しません。foo@bar.comは、本来はFoo@bar.comとは別のアドレスです。ただし現実的には、about.comでも指摘されているように、メールアドレスの大文字小文字を区別することを前提にするのはまずい方法です。「メールアドレスの大文字小文字を区別すると、果てしない混乱と相互運用性の問題とひどい頭痛が発生する。メールアドレスの入力時に大文字小文字の区別を要求するのは賢い方法とは言えない。現実には、メールアドレスの大文字小文字の区別を強制するメールサービスやISPはめったに存在しない。メールアドレスのすべての文字を大文字にするなど、受信者のメールアドレスが誤って入力されていれば、メールは返送されるだけだ。」Riley Mosesによるご指摘に感謝いたします。↑
- もちろん、リスト6.2の
users
テーブル用のマイグレーションファイルを単に編集することも可能なのですが、その場合ロールバックが必要となり、マイグレーションが戻ってしまいます。データモデルの変更が必要になったらその都度マイグレーションを行うのがRails流です。↑ - 著者のシステム上のSQLiteとHeroku上のPostgreSQLで直接実験してみたところ、この手順は実際に必要であることがわかりました。↑
- http://railstutorial.jp/book?version=3.0 ↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!