Ruby on Rails チュートリアル

実例を使ってRailsを学ぼう

Michael Hartl (マイケル・ハートル)

第3版 目次

前書き

私が前にいた会社 (CD Baby) は、かなり早い段階でRuby on Railsに乗り換えたのですが、またPHPに戻ってしまいました (詳細は私の名前をGoogleで検索してみてください)。そんな私ですが、Michael Hartl 氏の本を強く勧められたので、その本を使ってもう一度試してみた結果、今度は無事に Rails に乗り換えることができました。それがこの Ruby on Rails チュートリアルという本です。

私は多くの Rails 関連の本を参考にしてきましたが、真の決定版と呼べるものは本書をおいて他にありません。本書では、あらゆる手順が Rails 流で行われています。最初のうちは慣れるまでに時間がかかりましたが、この本を終えた今、ついにこれこそが自然な方式だと感じられるまでになりました。また、本書は Rails 関連の本の中で唯一、多くのプロが推奨するテスト駆動開発 (TDD: Test Driven Development) を、全編を通して実践しています。実例を使ってここまで分かりやすく解説された本は、本書が初めてでしょう。極めつけは、Git や GitHub、Heroku の実例に含めている点です。このような、実際の開発現場で使わているツールもチュートリアルに含まれているため、読者は、まるで実際のプロジェクトの開発プロセスを体験しているかのような感覚が得られるはずです。それでいて、それぞれの実例が独立したセクションになっているのではなく、そのどれもがチュートリアルの内容と見事に一体化しています。

本書は、筋道だった一本道の物語のようになっています。私自身、章の終わりにある練習問題もやりながら、この Rails チュートリアルを3日間かけて一気に読破しました1。最初から最後まで、途中を飛ばさずにやるのが一番効果的で有益な読み方です。ぜひやってみてください。

それでは、楽しんでお読みください!

Derek Sivers (sivers.org) CD Baby 創業者

謝辞

Ruby on Rails チュートリアルは、私の以前の著書「RailsSpace」と、その時の共著者の Aurelius Prochazka から多くのことを参考にさせてもらっています。Aure には、RailsSpace での協力と本書への支援も含め、感謝したいと思います。また、RailsSpaceRails チュートリアルの両方の編集を担当して頂いた Debra Williams Cauley 氏にも謝意を表したく思います。彼女が野球の試合に連れて行ってくれる限り、私は本を書き続けるでしょう。

私にインスピレーションと知識を与えてくれた Rubyist の方々にも感謝したいと思います: David Heinemeier Hansson、Yehuda Katz、Carl Lerche、Jeremy Kemper、Xavier Noria、Ryan Bat、Geoffrey Grosenbach、Peter Cooper、Matt Aimonetti、Mark Bates、Gregg Pollack、Wayne E. Seguin、Amy Hoy, Dave Chelimsky、Pat Maddox、Tom Preston-Werner、Chris Wanstrath、Chad Fowler、Josh Susser、Obie Fernandez、Ian McFarland、Steven Bristol、Pratik Naik、Sarah Mei、Sarah Allen、Wolfram Arnold、Alex Chaffee、Giles Bowkett、Evan Dorn、Long Nguyen、James Lindenbaum、Adam Wiggins、Tikhon Bernstam、Ron Evans、Wyatt Greene、Miles Forrest、Pivotal Labs の方々、Heroku の方々、thoughtbot の方々、そして GitHub の方々、ありがとうございました。最後に、ここに書ききれないほど多くの読者からバグ報告や提案を頂きました。ご協力いただいた皆様のおかげで、本書の完成度をとことんまで高めることができました。

著者

マイケルハートル (Michael Hartl) は「Ruby on Rails チュートリアル」という Web 開発を始めるときに最もよく参考にされる本の著者です。また、Softcover という自費出版プラットフォームの共同創業者でもあります。以前は、(今ではすっかり古くなってしまいましたが)「RailsSpace」という本の執筆および開発に携わったり、また、 一時人気を博した Ruby on Rails ベースのソーシャルネットワーキングプラットフォーム「Insoshi」の開発にも携わっていました。なお、2011年には、Rails コミュニティへの高い貢献が認められて、Ruby Hero Award を受賞しました。ハーバード大学卒業後、カリフォルニア工科大学物理学博士号を取得し、起業プログラム Y Combinator の卒業生でもあります。

  1. 3日間で読破するのは異常です! 実際にはもっと時間をかけて読むのが一般的です。

第6章 ユーザーのモデルを作成する

第5章では、新しいユーザーを作成するためのスタブページを作ったところで終わりました (5.4)。これから5つの章を通して、ユーザー登録ページを作っていくことにしましょう。本章では、一番重要なステップであるユーザー用のデータモデルの作成と、データを保存する手段の確保について学んでいきます。第7章では、ユーザーがサイトにユーザー登録できるようにし、ユーザープロファイルのためのページを作成します。ユーザー登録できるようになったら、ログインやログアウトをできる仕組みを作り (第8章)、第9章からは不正なアクセスを取り扱う方法について学んでいきます (9.2.1) 。最後に、第10章でメールアドレスを使ってアカウントを有効化する方法と、パスワードをリセットする方法について学びます。まとめると、第6章から第10章を通して、Railsのログインと認証システムをひととおり開発します。ご存知の方もいると思いますが、Railsでは既にさまざまな認証方法が利用可能です。コラム6.1では、最初に少なくとも一度は自分で認証システムを作ってみることをお勧めする理由について説明しています。

コラム6.1 自分で認証システムを作ってみる

事実上、すべてのWebアプリケーションは何らかのログイン/認証システムを必要とします。そのため、多くのWebフレームワークではこのようなログイン/認証システムを実装するための選択肢が多数提供されています。Railsもまた例外ではありません。認証 (authentication) と認可 (authorization) のシステムの例だと、ClearanceAuthlogicDeviseCanCanなどがあります (Railsに限らなければOpenIDOAuthの上に構築する方法もあります)。なぜ車輪の再発明をするのか、という質問があるのも当然です。自分でわざわざ作らなくても、いつも使える方法をただ利用するだけではいけないのでしょうか。

ある実践的な実験によると、多くのサイトの認証システムは膨大なカスタマイズを必要とするため、サードパーティ製品を変更して導入する場合にはシステムをゼロから作成するよりも多くの仕事を要するという結果が出ています。加えて、既成品のシステムは内部がわかりづらいことが多く、ブラックボックスになっています。自分で作成したシステムであれば、それをとてもよく理解しているはずです。さらに言えば、最近のRailsへの変更 (6.3) により、カスタム認証システムを容易に作成できるようになりました。最後に、あえて最終的にサードパーティの認証システムを導入することになったとしても、自分自身で認証システムを構築した経験があれば、サードパーティ製品を理解して変更することがずっと容易になるはずです。

6.1 Userモデル

ここから3つの章にわたる最終目標はユーザー登録ページ (6.1のモックアップ) を作成することですが、今のままでは新しいユーザーの情報を受け取っても保存する場所がないので、いきなりページを作成するわけにはいきません。ユーザー登録でまず初めにやることは、それらの情報を保存するためのデータ構造を作成することです。

images/figures/signup_mockup_bootstrap
図7.1 ユーザー登録ページのモックアップ

Railsでは、データモデルで使用するデフォルトのデータ構造のことをモデルと呼びます (1.3.3で言うMVCのMのことです)。Railsでは、データを永続化するデフォルトの解決策として、データベースを使用してデータを長期間保存します。また、データベースとやりとりするデフォルトのRailsライブラリはActive Recordと呼ばれます1。Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っています。これらのメソッドを使用するのに、リレーショナルデータベースで使うSQL (Structured Query Language)2を意識する必要はありません。さらに、Railsにはマイグレーションという機能があります。データの定義をRubyで記述することができ、SQLのDDL (Data Definition Language)を新たに学ぶ必要がありません。Railsは、データストアの詳細からほぼ完全に私たちを切り離してくれます。本書では、SQLiteを開発 (development) 環境で使い、またPostgreSQLを (Herokuでの) 本番環境で使います (1.5)。Railsは、本番 (production) アプリケーションですら、データの保存方法の詳細についてほとんど考える必要がないくらいよくできています。

Gitでバージョン管理を行なっているのであれば、このタイミングでユーザーをモデリングするためのトピックブランチを作成しておいてください。

$ git checkout master
$ git checkout -b modeling-users

6.1.1 データベースの移行

4.4.5で扱ったカスタムビルドクラスのUserを思い出してください。このクラスは、nameemailを属性に持つユーザーオブジェクトでした。このクラスは役に立つ例として提供されましたが、Railsにとって極めて重要な部分である永続性という要素が欠けていました。RailsコンソールでUserクラスのオブジェクトを作っても、コンソールからexitするとそのオブジェクトはすぐに消えてしまいました。この節での目的は、簡単に消えることのないユーザーのモデルを構築することです。

4.4.5のユーザークラスと同様に、nameemailの2つの属性からなるユーザーをモデリングするところから始めましょう。後者のemailを一意のユーザー名として使用します3 (パスワードのための属性は6.3で扱います)。リスト4.13では、以下のようにRubyのattr_accessorメソッドを使用しました。

class User
  attr_accessor :name, :email
  .
  .
  .
end

それとは対照的に、Railsでユーザーをモデリングするときは、属性を明示的に識別する必要がありません。上で簡潔に述べたように、Railsはデータを保存する際にデフォルトでリレーショナルデータベースを使用します。リレーショナルデータベースは、データで構成されるテーブルからなり、各行はデータ属性のカラム (列) を持ちます。たとえば、nameとemailを持つユーザーを保存するのであれば、nameemailのカラムを持つusersテーブルを作成します (各行は1人のユーザーを表します)。テーブルに格納されるデータの例を6.2に、対応するデータモデルを6.3に示します (なお、6.3は草案です。実際のデータモデルは6.4のようになります)。nameemailといったカラム名を今のうちに考えておくことで、後ほどUserオブジェクトの各属性をActiveRecordに伝えるときに楽になります。

images/figures/users_table
図6.2: usersテーブルに含まれるデータのサンプル
images/figures/user_model_sketch
図6.3: Userのデータモデルのスケッチ

リスト5.28で、ユーザーコントローラ (とnewアクション) を作ったときに使った以下のコマンドを思い出してみてください。

$ rails generate controller Users new

モデルを作成するときは、上と似たようなパターンでgenerate modelというコマンドを使います。さらに、今回はnameemailといった属性を付けたUserモデルを使いたいので、実際に打つコマンドはリスト6.1になります。

リスト6.1: Userモデルを生成する
$ rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20140724010738_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

(コントローラ名には複数形を使い、モデル名には単数形を用いるという慣習を頭に入れておいてください。コントローラはUsersでモデルはUserです)。name:stringemail:stringオプションのパラメータを渡すことによって、データベースで使用したい2つの属性をRailsに伝えます。このときに、これらの属性の型情報も一緒に渡します (この場合はstring)。リスト3.4リスト5.28でアクション名を使用して生成した例と比較してみてください。

リスト6.1にあるgenerateコマンドの結果のひとつとして、マイグレーションと呼ばれる新しいファイルが生成されます。マイグレーションは、データベースの構造をインクリメンタルに変更する手段を提供します。それにより、要求が変更された場合にデータモデルを適合させることができます。このUserモデルの例の場合、マイグレーションはモデル生成スクリプトによって自動的に作られました。リスト6.2に示したようにnameemailの2つのカラムを持つusersテーブルを作成します (6.2.5で、マイグレーションを一から手動で作成する方法について説明します)。

リスト6.2: (usersテーブルを作るための) Userモデルのマイグレーション db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps null: false
    end
  end
end

マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加されます。以前はインクリメンタルな整数が追加されましたが、複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、コンフリクトを引き起こしていました。現在のタイムスタンプによる方法であれば、まったく同時にマイグレーションが生成されるという通常ではありえないことが起きない限り、そのようなコンフリクトは避けられます。

マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まりです。リスト6.2の場合、changeメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成します。create_tableメソッドはブロック変数を1つ持つブロック (4.3.2) を受け取ります。ここでは (“table”の頭文字を取って) tです。そのブロックの中でcreate_tableメソッドはtオブジェクトを使って、nameemailカラムをデータベースに作ります。型はどちらもstringです4。モデル名は単数形 (User) ですが、テーブル名は複数形 (users) です。これはRailsで用いられる言葉の慣習を反映しています。モデルはひとりのユーザーを表すのに対し、データベースのテーブルは複数のユーザーから構成されます。ブロックの最後の行t.timestampsは特別なコマンドで、created_atupdated_atという2つの「マジックカラム」を作成します。これらは、あるユーザーが作成または更新されたときに、その時刻を自動的に記録するタイムスタンプです (このマジックカラムの使用例を6.1.3から具体的に見ていきます)。リスト6.2のマイグレーションによって作成された完全なデータモデルを6.4に示します (6.3のスケッチには無かったマジックカラムが追加されています)。

user_model_initial_3rd_edition
図6.4 リスト6.2で生成されたUserのデータモデル

マイグレーションは、以下のようにrakeコマンド (コラム2.1) を使って実行することができます。これを「マイグレーションの適用 (migrating up)」と呼びます。

$ bundle exec rake db:migrate

(2.2で、このコマンドを似たような状況で実行したことを思い出してみてください) 。初めてdb:migrateが実行されると、db/development.sqlite3という名前のファイルが生成されます。これはSQLite5データベースです。db/development.sqlite3ファイルを開くためのDB Browser for SQLiteという素晴らしいツールを使うと、データベースの構造を見ることができます (Cloud IDEを使っている場合は、6.5のようにまずはファイルをお手元にダウンロードする必要があります)。結果は6.6のようになるので、6.4と比べてみてください。6.6の中にidというマイグレーションのときに説明されなかったカラムの存在に気づいたかもしれません。2.2で簡単に説明したとおり、このカラムは自動的に作成され、Railsが各行を一意に識別するために使用します。

images/figures/sqlite_download
図6.5: Cloud IDEからファイルをダウンロードする
images/figures/sqlite_database_browser_3rd_edition
図6.6: DB Browser for SQLiteで作成したusersテーブルを確認する

Railsチュートリアルで使用されているものすべてを含め、ほとんどのマイグレーションが可逆です。これは、db:rollbackというRakeタスクで変更を取り消せることを意味します。これを“マイグレーションの取り消し (migrate down) と呼びます。

$ bundle exec rake db:rollback

(コラム3.1では、マイグレーションを元に戻すための便利なテクニックを他にも紹介しています)。上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、changeメソッドはdrop_tablecreate_tableの逆であることを知っているからです。つまり、ロールバック用の逆方向マイグレーションを簡単に導くことができるのです。あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、updownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。

もし今の時点でデータベースのロールバックを実行していた場合は、先に進む前にもう一度以下のようにマイグレーションを適用して元に戻してください。

$ bundle exec rake db:migrate

6.1.2 modelファイル

これまで、リスト6.1のUserモデルの作成によってどのように (リスト6.2の) マイグレーションファイルが作成されるかを見てきました。そして6.3でこのマイグレーションを実行した結果を見ました。usersテーブルを作成することで、development.sqlite3という名のファイルを更新し、idnameemailcreated_atupdated_atを作成しました。また、リスト6.1ではモデル用のuser.rbも作られました。この節では、以後このモデル用ファイルを理解することに専念します。

まずは、app/models/ディレクトリにある、user.rbファイルに書かれたUserモデルのコードを見てみましょう。これは控えめに言ってもとてもよくまとまっています (リスト6.3)

リスト6.3: 生成されたばかりのUserモデル app/models/user.rb
class User < ActiveRecord::Base
end

4.4.2で行ったことを思い出してみましょう。class User < ActiveRecord::Baseという構文で、UserクラスはActiveRecord::Base継承するので、Userモデルは自動的にActiveRecord::Baseクラスのすべての機能を持ちます。もちろん、継承されていることが分かっても、ActiveRecord::Baseに含まれるメソッドなどについて知らなければ何の役にも立ちません。それらの知識の一部についてこれから説明します。

6.1.3 ユーザーオブジェクトを作成する

第4章と同じく、Railsコンソールを使用してデータモデルを調べてみましょう。現時点ではデータベースを変更したくないので、コンソールをサンドボックスモードで起動します。

$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>

"Any modifications you make will be rolled back on exit" (ここで行ったすべての変更は終了時にロールバックされます) というメッセージにわかりやすく示されているように、コンソールをサンドボックスで起動すると、そのセッションで行ったデータベースへの変更をコンソールの終了時にすべて “ロールバック” (取り消し) してくれます。

4.4.5のコンソールセッションではUser.newで新しいユーザーオブジェクトを生成しましたが、リスト4.13のexample_userファイルを明示的にrequireするまでこのオブジェクトにはアクセスできませんでした。しかし、モデルを使うと状況は異なります。4.4.4で見たように、Railsコンソールは起動時にRailsの環境を自動的に読み込み、その環境にはモデルも含まれます。つまり、新しいユーザーオブジェクトを作成するときに余分な作業を行わずに済むということです。

>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

上の出力は、ユーザーオブジェクトをコンソール用に出力したものです。

User.newを引数なしで呼んだ場合は、すべての属性がnilのオブジェクトを返します。4.4.5では、オブジェクトの属性を設定するための初期化ハッシュ (hash) を引数に取るように、Userクラスの例 (user_example.rb) を設計しました。この設計は、同様の方法でオブジェクトを初期化するActive Recordの設計に基づいています。

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com",
created_at: nil, updated_at: nil>

上のように、nameとemail属性が期待どおり設定されていることがわかります。

また、Active Recordを理解する上で、「有効性 (Validity)」という概念も重要です。6.2で詳細について解説しますが、今はまず先ほどのuserオブジェクトが有効かどうか確認してみましょう。確認するためにはvalid?メソッドを使います。

>> user.valid?
true

現時点ではまだデータベースにデータは格納されていません。つまり、User.newメモリ上でオブジェクトを作成しただけで、user.valid?という行はただオブジェクトが有効かどうかを確認しただけとなります (データベースにデータがあるかどうかは有効性には関係ありません)。データベースにUserオブジェクトを保存するためには、userオブジェクトからsaveメソッドを呼び出す必要があります。

>> user.save
   (0.2ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users"  WHERE LOWER("users".
  "email") = LOWER('mhartl@example.com') LIMIT 1
  SQL (0.5ms)  INSERT INTO "users" ("created_at", "email", "name", "updated_at)
   VALUES (?, ?, ?, ?)  [["created_at", "2014-09-11 14:32:14.199519"],
   ["email", "mhartl@example.com"], ["name", "Michael Hartl"], ["updated_at",
  "2014-09-11 14:32:14.199519"]]
   (0.9ms)  commit transaction
=> true

saveメソッドは、成功すればtrueを、失敗すればfalseを返します (現状では、保存はすべて成功するはずです。失敗する場合については6.2で説明します)。また、Railsコンソール上ではuser.saveに対応するSQLコマンドやその結果 (INSERT INTO "users"...) も表示するようになっています。なお、本書では生のSQLが必要になる場面がほとんどないので6、SQLコマンドに関する解説は省略します。とはいえ、Active Recordに対応するSQLコマンドをザッと眺めておくだけでも勉強にはなるはずです。

作成した時点でのユーザーオブジェクトは、id属性、マジックカラムであるcreated_at属性とupdated_at属性の値がいずれもnilであったことを思い出してください。saveメソッドを実行した後に何が変更されたのかを確認してみましょう。

>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">

idには1という値が代入され、一方でマジックカラムには現在の日時が代入されているのがわかります7。現在、作成と更新のタイムスタンプは同一ですが、6.1.5では異なる値になります。

4.4.5のUserクラスと同様に、Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます。

>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Thu, 24 Jul 2014 00:57:46 UTC +00:00

詳細は第7章でも説明しますが、上で見たようにモデルの生成と保存を2つのステップに分けておくと何かと便利です。しかし、Active RecordではUser.createでモデルの生成と保存を同時におこなう方法も提供されています。

>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2014-07-24 01:05:24", updated_at: "2014-07-24 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">

User.createは、truefalseを返す代わりに、ユーザーオブジェクト自身を返すことに注目してください。返されたユーザーオブジェクトは (上の2つ目のコマンドにあるfooのように) 変数に代入することもできます。

destroycreateの逆です。

>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">

createと同じように、destroyはそのオブジェクト自身を返しますが、その返り値を使用しても、もう一度destroyを呼ぶことはできません。さらに、削除されたオブジェクトは、以下のようにまだメモリ上に残っています。

>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2014-07-24
01:05:42", updated_at: "2014-07-24 01:05:42">

では、オブジェクトが本当に削除されたかどうかをどのようにして知ればよいのでしょうか。そして、保存して削除されていないオブジェクトの場合、どうやってデータベースからユーザーを取得するのでしょうか。これらの問いに答えるためには、Active Recordを使ってUserオブジェクトを検索する方法について学ぶ必要があります。

6.1.4 ユーザーオブジェクトを検索する

Active Recordには、オブジェクトを検索するための方法がいくつもあります。これらの機能を使用して、過去に作成した最初のユーザーを探してみましょう。また、3番目のユーザー (foo) が削除されていることを確認しましょう。まずは存在するユーザーから探してみましょう。

>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">

ここでは、User.findにユーザーのidを渡しています。その結果、Active Recordはそのidのユーザーを返します。

次に、id=3のユーザーがまだデータベースに存在するかどうかを確認してみましょう。

>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3

6.1.3で3番目のユーザーを削除したので、Active Recordはこのユーザーをデータベースの中から見つけることができませんでした。代わりに、findメソッドは例外 (exception) を発生します。例外はプログラムの実行時に何か例外的なイベントが発生したことを示すために使われます。この場合、存在しないActive Recordのidによって、findActiveRecord::RecordNotFound例外8が発生しました。

一般的なfindメソッド以外に、Active Recordには特定の属性でユーザーを検索する方法もあります。

>> User.find_by(email: "mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">

これまでメールアドレスをユーザー名として使用してきたので、このようなfind関連メソッドは、ユーザーをサイトにログインさせる方法を学ぶときに役に立ちます (第7章)。ユーザー数が膨大になるとfind_byでは検索効率が低下するのではないかと心配する方もいるかもしれませんが、あせる必要はありません。この問題およびデータベースのインデックスを使った解決策については6.2.5で扱います。

ユーザーを検索する一般的な方法をあと少しだけご紹介して、この節を終わりにすることにしましょう。まず初めにfirstメソッドです。

>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">

読んで字のごとく、firstは単にデータベースの最初のユーザーを返します。次はallメソッドです。

>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2014-07-24 00:57:46",
updated_at: "2014-07-24 00:57:46">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2014-07-24 01:05:24",
updated_at: "2014-07-24 01:05:24">]>

コンソールの出力結果を見ると、User.allでデータベースのすべてのUserオブジェクトが返ってくることがわかります。また、返ってきたオブジェクトのクラスが ActiveRecord::Relationとなっています。これは、各オブジェクトを配列として効率的にまとめてくれるクラスです(4.3.1)。

6.1.5 ユーザーオブジェクトを更新する

いったんオブジェクトを作成すれば、今度は何度でも更新したくなるものです。基本的な更新の方法は2つです。ひとつは、4.4.5でやったように属性を個別に代入する方法です。

>> user           # Just a reminder about our user's attributes
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-24 00:57:46", updated_at: "2014-07-24 00:57:46">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true

変更をデータベースに保存するために最後にsaveを実行する必要があることを忘れないでください。保存を行わずにreloadを実行すると、データベースの情報を元にオブジェクトを再読み込みするので、以下のように変更が取り消されます。

>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"

user.saveを実行したことでユーザーが更新できました。6.1.3で約束したように、マジックカラムの更新日時も更新されています。

>> user.created_at
=> "2014-07-24 00:57:46"
>> user.updated_at
=> "2014-07-24 01:37:32"

属性を更新するもうひとつの方法は、update_attributesを使うものです9

>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"

update_attributesメソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行います (保存に成功した場合はtrueを返します)。ただし、検証に1つでも失敗すると、update_attributesの呼び出しは失敗します。たとえば、6.3で実装する、パスワードをレコードに保存することを要求すると検証は失敗します。特定の属性のみを更新したい場合は、以下のようにupdate_attributeを使います。なお、update_attributeには検証を回避するといった効果もあります。

>> user.update_attribute(:name, "The Dude")
=> true
>> user.name
=> "The Dude"

6.2 ユーザーを検証する

ついに、6.1で作成したUserモデルに、アクセス可能なnameemail属性が与えられました。しかし、これらの属性はどんな値でも取ることができてしまいます。現在は (空文字を含む) あらゆる文字列が有効です。名前とメールアドレスには、もう少し何らかの制限があってよいはずです。たとえば、nameは空であってはならず、emailはメールアドレスのフォーマットに従う必要があります。さらに、メールアドレスをユーザーがログインするときの一意のユーザー名として使おうとしているので、メールアドレスがデータベース内で重複することのないようにする必要もあります。

要するに、nameemailにあらゆる文字列を許すのは避けるべきです。これらの属性値には、何らかの制約を与える必要があります。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)。

リスト6.4: デフォルトのUserテスト (モックのみ) test/models/user_test.rb
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のようになります。

リスト6.5: 有効なUserかどうかをテストする GREEN 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
end

リスト6.5では、シンプルなassertメソッドを使ってテストします。@user.valid?trueを返すと成功し、falseを返すと失敗します。

とはいえ、Userモデルにはまだバリデーションがないので、このテストは成功するはずです。

リスト6.6: GREEN
$ bundle exec rake test:models

上ではrake test:modelsというコマンドを実行していますが、これはモデルに関するテストだけを走らせるコマンドです (5.3.4で使ったrake test:integrationと似ていることに注目してください)。

6.2.2 存在性を検証する

おそらく最も基本的なバリデーションは「存在性 (Presence)」です。これは単に、与えられた属性が存在することを検証します。たとえばこの節では、ユーザーがデータベースに保存される前にnameとemailフィールドの両方が存在することを保証します。7.3.3では、この要求を新しいユーザーを作るためのユーザー登録フォームにまで徹底させる方法を確認します。

まずはリスト6.5に、name属性の存在性に関するテストを追加します。具体的にはリスト6.7のように、まず@user変数のname属性に対して空白の文字列をセットします。そして、assert_notメソッドを使って Userオブジェクトが有効でなくなったことを確認します。

リスト6.7: 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

この時点では、モデルのテストは失敗するはずです。

リスト6.8: RED
$ bundle exec rake test:models

第2章の演習で少し触れましたが、name属性の存在を検査する方法は、リスト6.9に示したとおり、validatesメソッドにpresence: trueという引数を与えて使うことです。presence: trueという引数は、要素がひとつのオプションハッシュです。4.3.4のようにメソッドの最後の引数としてハッシュを渡す場合、波括弧を付けなくても問題ありません(5.1.1でも説明したように、Railsのオプションハッシュは繰り返し登場するテーマです)。

リスト6.9: name属性の存在性を検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name, presence: true
end

リスト6.9は一見魔法のように見えるかもしれませんが、validatesは単なるメソッドです。括弧を使用してリスト6.9を同等のコードに書き換えたものを以下に示します。

class User < ActiveRecord::Base
  validates(:name, presence: true)
end

コンソールを起動して、Userモデルに検証を追加した効果を見てみましょう10

$ rails console --sandbox
>> user = User.new(name: "", email: "mhartl@example.com")
>> user.valid?
=> false

このように、user変数が有効かどうかをvalid?メソッドでチェックすることができます。もしオブジェクトがひとつ以上の検証に失敗したときは、falseを返します。 また、すべてのバリデーションに通ったときにtrueを返します。今回の場合、検証が1つしかないので、どの検証が失敗したかわかります。しかし、失敗したときに作られるerrorsオブジェクトを使って確認すれば、さらに便利です。

>> user.errors.full_messages
=> ["Name can't be blank"]

(Railsが属性の存在性を検査するときに、エラーメッセージはヒントになります。これにはblank? メソッドを用います。4.4.3の終わりに見ました)。

Userオブジェクトは有効ではなくなったので、データベースに保存しようとすると自動的に失敗するはずです。

>> user.save
=> false

この変更によりリスト6.7のテストは成功しているはずです。

リスト6.10: GREEN
$ bundle exec rake test:models

リスト6.7のモデルに倣って、email属性の存在性についてもテストを書いてみましょう (リスト6.11)。最初は失敗しますが、リスト6.12のコードを追加することで成功するようになります。

リスト6.11: 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
リスト6.12: email属性の存在性を検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name,  presence: true
  validates :email, presence: true
end

これですべての存在性がチェックされたので、テストスイートは成功するはずです。

リスト6.13: GREEN
$ bundle exec rake test

6.2.3 長さを検証する

各ユーザーは、Userモデル上に名前を持つことを強制されるようになりました。しかし、これだけでは十分ではありません。ユーザーの名前はサンプルWebサイトに表示されるものなので、名前の長さにも制限を与える必要があります。6.2.2で既に同じような作業を行ったので、この実装は簡単です。

最長のユーザー名の長さに科学的な根拠はありませんので、単に50を上限として手頃な値を使うことにします。つまりここでは、51文字の名前は長すぎることを検証します。また、実際に問題になることはほとんどありませんが、問題になる可能性もあるので長すぎるメールアドレスに対してもバリデーションを掛けましょう。ほとんどのデータベースでは文字列の上限を255としているので、それに合わせて255文字を上限とします。6.2.4で説明するメールアドレスのフォーマットに関するバリデーションでは、こういった長さの検証はできないので、本節で長さに関するバリデーションを事前に追加しておきます。結果をリスト6.14に示します。

リスト6.14: nameの長さの検証に対するテスト RED test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  test "name should not be too long" do
    @user.name = "a" * 51
    assert_not @user.valid?
  end

  test "email should not be too long" do
    @user.email = "a" * 244 + "@example.com"
    assert_not @user.valid?
  end
end

リスト6.14では、51文字の文字列を簡単に作るために “文字列のかけ算” を使いました。結果をコンソール上で確認できます。

>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51

メールアドレスの長さに対するバリデーションも、次のように長い文字列を作成して検証します。

>> "a" * 244 + "@example.com"
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaa@example.com"
>> ("a" * 244 + "@example.com").length
=> 256

この時点では、リスト6.14のテストは失敗しているはずです。

リスト 6.15: RED
$ bundle exec rake test

これをパスさせるためには、長さを強制するための検証の引数を使う必要があります。:maximumパラメータと共に用いられる:lengthは、長さの上限を強制します (リスト6.16)。

リスト6.16: name属性に長さの検証を追加する GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end

これでテストがGREENになるはずです。

リスト 6.17: GREEN
$ bundle exec rake test

成功したテストスイートを流用して、今度は少し難しい、メールアドレスのフォーマット検証作業に取りかかりましょう。

6.2.4 フォーマットを検証する

name属性の検証には、空文字でない、名前が51文字未満であるという最小限の制約しか与えていませんでした。email属性の場合は、有効なメールアドレスかどうかを判定するために、もっと厳重な要求を満たさなければなりません。これまでは空のメールアドレスのみを禁止してきましたが、ここではメールアドレスにおなじみのパターンuser@example.comに合っているかどうかも確認することを要求します。

なお、ここで使用するテストや検証は、形式がひとまず有効なメールアドレスを受け入れ、形式があからさまに無効なものを拒否するだけであり、すべての場合を網羅したものではないという点に注意してください。最初に、有効なメールアドレスと無効なメールアドレスのコレクションに対するテストを行いましょう。このコレクションを作るために、以下のコンソールセッションに示したような、文字列の配列を簡単に作れる%w[]という便利なテクニックを知っておくと良いでしょう。

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[USER@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["USER@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?>   puts address
>> end
USER@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp

eachメソッドを使ってaddresses配列の各要素を繰り返し取り出しました (4.3.2)。このテクニックを学んだことで、基本となるメールアドレスフォーマット検証のテストを書く準備が整いました。

メールアドレスのバリデーションは扱いが難しく、エラーが発生しやすい部分なので、有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検知していきます。具体的には、user@example,comのような無効なメールアドレスが弾かれることと、user@example.comのような有効なメールアドレスが通ることを確認しながら、バリデーションを実装していきます (ちなみに今の状態では、空でないメールアドレスであれば全て通ってしまいます) 。まずは、有効なメールアドレスをリスト6.18に示します。

リスト6.18: 有効なメールフォーマットをテストする GREEN 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 "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でもエラーメッセージをカスタマイズして、どのメールアドレスで失敗したのかすぐに特定できるようにしておきます。

リスト6.19: メールフォーマットの検証に対するテスト 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 "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

この時点では、テストは失敗するはずです。

リスト6.20: RED
$ bundle exec rake test

メールアドレスのフォーマットを検証するためには、次のようにformatというオプションを使います。

validates :email, format: { with: /<regular expression>/ }

このオプションは引数に正規表現 (Regular Expression) (regexとも呼ばれます) を取ります。正規表現は一見謎めいて見えますが、文字列のパターンマッチングにおいては非常に強力な言語です。つまり、有効なメールアドレスだけにマッチして、無効なメールアドレスにはマッチしない正規表現を組み立てる必要があります。

メールアドレス標準を定める公式サイトに完全な正規表現があるのですが、非常に巨大かつ意味不明で、場合によっては逆効果になりかねるので11、本チュートリアルではもっと実用的で、堅牢であることが実戦で保証されている正規表現を採用します。 これが、その正規表現です。

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

この正規表現を理解するために、お手頃なサイズに分割して6.1にまとめました12

表現 意味
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i (完全な正規表現)
/ 正規表現の開始を示す
\A 文字列の先頭
[\w+\-.]+ 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d\-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
\. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わりを示す
i 大文字小文字を無視するオプション
表6.1 メールの正規表現を分解した結果

6.1からも多くのことを学べるとは思いますが、正規表現を本当に理解するためには実際に使って見るのが一番です。たとえばRubularという対話的に正規表現を試せるWebサイトがあります (6.7)13。このWebサイトはインタラクティブ性に富んだインターフェイスを持っていて、また、正規表現のクイックリファレンスも兼ね備えています。Rubularをブラウザで開き、6.1の内容を実際に試してみることを強くお勧めします。正規表現は、読んで学ぶより対話的に学んだほうが早いです。(: 6.1の正規表現をRubularで使う場合、冒頭の\Aと末尾の\zの文字は含めないでください)。

images/figures/rubular
図6.4 素晴らしい正規表現エディタRubular

6.1の正規表現を適用してemailのフォーマットを検証した結果を、リスト6.21に示します。

リスト6.21: メールフォーマットを正規表現で検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }
end

正規表現VALID_EMAIL_REGEX定数です。大文字で始まる名前はRubyでは定数を意味します。以下のコードは、

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }

このパターンに一致するメールアドレスだけが有効であることをチェックします。上の正規表現には少しだけ残念な点があります。foo@bar..comのようなドットの連続を誤りとして検出できません。この問題の修正するにはとてつもなく複雑な正規表現を使う必要がありますが、これは演習問題に回します (6.5)。

この時点で、テストは成功しているはずです。

リスト6.22: GREEN
$ bundle exec rake test:models

残る制約は、メールアドレスが一意であることを強制するだけとなりました。

6.2.5 一意性を検証する

メールアドレスの一意性を強制するために (ユーザー名として使うために)、validatesメソッドの:uniqueオプションを使います。ただしここで重大な警告があります。以下の文面は流し読みせず、必ず注意深く読んでください。

まずは小さなテストから書いていきます。モデルのテストではこれまで、主にUser.newを使ってきました。このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要があります14。そのため、まずは重複したメールアドレスからテストしていきます (リスト6.23)。

リスト6.23: 重複するメールアドレス拒否のテスト 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 "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
end

上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています。dupは、同じ属性を持つデータを複製するためのメソッドです。@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはずです。

リスト6.23のテストをパスさせるために、emailのバリデーションにuniqueness: trueというオプションを追加します リスト6.24

リスト6.24: メールアドレスの一意性を検証する GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

実装の途中ですが、ここでひとつ補足します。通常、メールアドレスでは大文字小文字が区別されません。すなわち、foo@bar.comFOO@BAR.COMFoO@BAr.coMと書いても扱いは同じです。従って、メールアドレスの検証ではこのような場合も考慮する必要があります15 。このため、大文字を区別しないでテストすることが肝要になり、実際のコードはリスト 6.25のようになります。

リスト6.25: 大文字小文字を区別しない、一意性のテスト 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 "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end
end

上のコードではStringのupcaseメソッドを使っています (4.3.2)。このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。

$ rails console --sandbox
>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
>> duplicate_user.email = user.email.upcase
>> duplicate_user.valid?
=> true

現在の一意性検証では大文字小文字を区別しているため、duplicate_user.valid?trueになります。しかし、ここではfalseになる必要があります。幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使用できます (リスト6.26)。

リスト6.26: メールアドレスの大文字小文字を無視した一意性の検証 GREEN app/models/user.rb
class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

ここで、リスト6.24truecase_sensitive: falseに置き換えただけであることに注目してください (リスト6.26)。Railsはこの場合、:uniquenesstrueと判断します。

この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制し、テストスイートもパスするはずです。

リスト 6.27: GREEN
$ bundle exec rake test

しかし、依然としてここには1つの問題が残っています。それはActive Recordはデータベースのレベルでは一意性を保証していないという問題です。具体的なシナリオを使ってその問題を説明します。

  1. アリスはサンプルアプリケーションにユーザー登録します。メールアドレスはalice@wonderland.comです。
  2. アリスは誤って “Submit” を素早く2回クリックしてしまいます。そのためリクエストが2つ連続で送信されます。
  3. 次のようなことが順に発生します。リクエスト1は、検証にパスするユーザーをメモリー上に作成します。リクエスト2でも同じことが起きます。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存されます。
  4. この結果、一意性の検証が行われているにもかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

上のシナリオが信じがたいもののように思えるかもしれませんが、どうか信じてください。RailsのWebサイトでは、トラフィックが多いときにこのような問題が発生する可能性があるのです (筆者もこれを理解するのに苦労しました)。幸い、解決策の実装は簡単です。実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。具体的にはデータベース上のemailのカラムにインデックス (index)を追加し (コラム6.2)、そのインデックスが一意であるようにすれば解決します。

コラム6.2 データベースのインデックス

データベースにカラムを作成するとき、そのカラムでレコードを検索する (find) 必要があるかどうかを考えることは重要です。たとえば、リスト6.2のマイグレーションによって作成されたemail属性について考えてみましょう。第7章ではユーザーをサンプルアプリにログインできるようにしますが、このとき、送信されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探しだす必要があります。残念なことに、(インデックスなどの機能を持たない) 素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と与えられたメールアドレスを比較するという非効率的な方法しかありません。つまり、たとえばデータベース上の最後のユーザを探す場合でも、すべてのユーザーを最初から順に一人ずつ探していくことになります。これは、データベースの世界では全表スキャンとして知られており、数千のユーザーがいる実際のサイトでは極めて不都合です。

emailカラムにインデックスを追加することで、この問題を解決することができます。データベースのインデックスを理解するためには、本の索引との類似性を考えるとよいでしょう。索引のない本では、与えられた言葉 (例えば、“foobar”) が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります (紙バージョンの全表スキャン)。しかし索引のある本であれば、“foobar”を含むすべてのページを索引の中から探すだけで済みます。データベースのインデックスも本質的には本の索引と同じように動作します。

emailインデックスを追加すると、データモデリングの変更が必要になります。Railsでは (6.1.1で見たように) マイグレーションでインデックスを追加します。6.1.1で、Userモデルを生成すると自動的に新しいマイグレーションが作成されたことを思い出してください (リスト6.2)。今回の場合は、既に存在するモデルに構造を追加するので、以下のようにmigrationジェネレーターを使用してマイグレーションを直接作成する必要があります。

$ rails generate migration add_index_to_users_email

ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。リスト6.28のように定義を記述する必要があります16

リスト6.28: メールアドレスの一意性を強制するためのマイグレーション 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用のサンプルデータが含まれている) fixtures内で一意性の制限が保たれていないため、テストは失敗します。リスト6.1でユーザー用のfixtureが自動的に生成されていますが、メールアドレスが一意になっていません (リスト6.29)。(このデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなりませんでした。)

リスト6.29: Userのデフォルトfixture RED test/fixtures/users.yml
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/
# FixtureSet.html

one:
  name: MyString
  email: MyString

two:
  name: MyString
  email: MyString

また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう (リスト6.30)。

リスト6.30: 空のfixtureファイル GREEN test/fixtures/users.yml
# empty

これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。それは、「いくつかのデータベースのアダプタは大文字小文字を区別するインデックスを使っている」という問題への対処です。例えば“Foo@ExAMPle.Com”と“foo@example.com”が別々の文字列だと解釈してしまうデータベースがありますが、私達のアプリケーションではこれらの文字列は同一であると解釈されるべきです。この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採ります。例えば“Foo@ExAMPle.CoM”という文字列が与えられたら、保存する直前に“foo@example.com”に変換してしまいます。これを実装するためにActive Recordのコールバック (callback) メソッドを利用します。このメソッドは、ある特定の時点で呼び出されるメソッドです。今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_saveというコールバックを使います。これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換します17。これをコードにすると、リスト6.31のようになります。(本チュートリアルで初めて紹介したテクニックですが、このテクニックについては第10章でもう一度取り上げます。そこではコールバックを定義するときにメソッドを参照するという慣習について説明します。)

リスト6.31: email属性を小文字に変換してメールアドレスの一意性を保証する GREEN app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

リスト6.31のコードは、before_saveコールバックにブロックを渡してユーザーのメールアドレスを設定します。設定されるメールアドレスは、現在の値をStringクラスのdowncaseメソッドを使って小文字バージョンにしたものです。メールアドレスの小文字変換に対するテストは演習として残しておきます (6.5)。

リスト6.31では、次のように代入をしていましたが、

self.email = self.email.downcase

Userモデルの中では、右式でselfというキーワードは省略できます (ちなみにここのselfは現在のユーザーを指します)。したがって、次のように書くこともできます。

self.email = email.downcase

実は4.4.2palindrome内でreverse メソッドを使っていたときも、同様のケースであったことを思い出してください。そのときと同様で、左式ではselfを省略することはできません。したがって、

email = email.downcase

と書くとうまく動きません。(このトピックについては、8.4でより深く解説していきます。)

これで、先に述べたアリスのシナリオはうまくいくようになります。データベースは、最初のリクエストに基づいてユーザーのレコードを保存しますが、2度目の保存は一意性の制約に反するので拒否します(Railsのログにエラーが出力されますが、害は生じません)。さらに、インデックスをemail属性に追加したことで、6.1.4で挙げた2番目の目標である「多数のデータがあるときの検索効率を向上させる」も達成されました。これは、email属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになったためです (コラム6.2)。

6.3 セキュアなパスワードを追加する

ユーザー属性の「名前」と「メールアドレス」に対してバリデーションを追加したので、最後の砦である「セキュアなパスワード」に取り掛かります。セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存します。(ハッシュ化というと少し困惑してしまうかもしれません。4.3.3ではハッシュとはRubyのデータ構造であると説明しましたが、今回の「ハッシュ化」とはそういった構造ではなく、ハッシュ関数を使って入力されたデータを元に戻せない (不可逆な) データにする処理を指します。) また、入力されたパスワードを使用してユーザーを認証する手段と、第8章で使用する、ユーザーがサイトにログインできるようにする手段も提供します。

ユーザーの認証は、パスワードの送信、ハッシュ化、データベース内のハッシュ化された値との比較、という手順で進んでいきます。比較の結果が一致すれば、送信されたパスワードは正しいと認識され、そのユーザーは認証されます。ここで、生のパスワードではなく、ハッシュ化されたパスワード同士を比較していることに注目してください。こうすることで、生のパスワードをデータベースに保存するという危険なことをしなくてもユーザーを認証できます。これで、仮にデータベースの内容が盗まれたり覗き見されるようなことがあっても、パスワードの安全性が保たれます。

6.3.1 ハッシュ化されたパスワード

セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけでほとんど終わってしまいます。このメソッドは、Userモデルで次のように呼び出せます。

class User < ActiveRecord::Base
  .
  .
  .
  has_secure_password
end

上のようにモデルにこのメソッドを追加すると、次のような機能が使えるようになります。

  • セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
  • 2つのペアの仮想的な属性18 (passwordpassword_confirmation)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
  • authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalse返すメソッド)。

この魔術的なhas_secure_password機能を使えるようにするには、1つだけ条件があります。それは、モデル内にpassword_digestという属性が含まれていることです。ちなみにdigestという言葉は、暗号化用ハッシュ関数という用語が語源です。したがって、今回の文脈ではハッシュ化されたパスワード暗号化されたパスワードは似た表現となります19。今回はUserモデルで使うので、Userのデータモデルは以下の図のようになります (6.8)。

user_model_password_digest_3rd_edition
図6.8 Userモデルにpassword_digest属性を追加する

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.1usersテーブルを最初に生成するとき、name:stringemail:stringといった引数を与えていたことを思い出してください。そのときと同様にpassword_digest:stringという引数を与えることで、完全なマイグレーションを生成するための十分な情報をRailsに与えることができます (リスト6.32)。

リスト6.32: usersテーブルにpassword_digestカラムを追加するマイグレーション db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
  def change
    add_column :users, :password_digest, :string
  end
end

リスト6.32では、add_columnメソッドを使ってusersテーブルにpassword_digestカラムを追加しています。これを適用させるには、データベースでマイグレーションを実行します。

$ bundle exec rake db:migrate

また、has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcryptを使用するために、bcrypt-ruby gemをGemfileに追加します (リスト6.33)。

リスト6.33: bcryptGemfileに追加する
source 'https://rubygems.org'

gem 'rails',                '4.2.2'
gem 'bcrypt',               '3.1.7'
.
.
.

次に、いつものようにbundle installを実行します。

$ bundle install

6.3.2 ユーザーがセキュアなパスワードを持っている

Userモデルにpassword_digest属性を追加し、Gemfileにbcryptを追加したことで、ようやくUserモデル内でhas_secure_passwordが使えるようになりました (リスト6.34)。

リスト6.34: Userモデルにhas_secure_passwordを追加する RED app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
end

リスト6.34REDと記しておいたように、まだテストは落ちたままになっているはずです。コマンドラインで以下を実行して確認してください。

リスト6.35: RED
$ bundle exec rake test

テストが失敗する理由は、6.3.1で触れたようにhas_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されているからです。しかしリスト6.25のテストでは、@user 変数にこのような値がセットされておりません。

def setup
  @user = User.new(name: "Example User", email: "user@example.com")
end

テストをパスさせるために、リスト6.36のようにパスワードとパスワード確認の値を追加します。

リスト6.36: パスワードとパスワード確認を追加する GREEN test/models/user_test.rb
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

これでテストが成功するようになります。

リスト6.37: GREEN
$ bundle exec rake test

Userモデルに対してhas_secure_passwordを追加する利点は6.3.4で少しだけ説明しますが、 その前に、パスワードの最小文字数を設定する方法について説明します。

6.3.3 パスワードの最小文字数

パスワードを簡単に当てられないようにするために、パスワードの最小文字数を設定しておくことは一般に実用的です。Railsでパスワードの長さを設定する方法はたくさんありますが、今回は簡潔にパスワードが空でないことと最小文字数 (6文字) の2つを設定しましょう。パスワードの長さが6文字以上であることを検証するテストを、以下のリスト6.38に示します。

リスト6.38: パスワードの最小文字数をテストする 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",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end

ここで、以下のような多重代入 (Multiple Assignment) を使っていることに注目してください。

@user.password = @user.password_confirmation = "a" * 5

リスト6.38のこの行では、パスワードとパスワード確認に対して同時に代入をしています (このケースでは、リスト6.14と同じように、文字列の乗算を利用して5文字の文字列を代入しています)。

リスト6.16ではmaximumを使ってユーザー名の最大文字数を制限していましたが、これと似たような形式のminimumというオプションを使って、最小文字数のバリデーションを実装することができます。

validates :password, length: { minimum: 6 }

また、空のパスワードを入力させないために、存在性のバリデーション (6.2.2) も一緒に追加します。結果として、Userモデルのコードはリスト6.39のようになります。(has_secure_passwordメソッドは存在性のバリデーションもしてくれるのですが、これは新しくレコードが追加されたときだけに適用されます。したがって、たとえばユーザーが " " (6文字分の空白スペース) といった文字列をパスワード欄に入力して更新しようとすると、バリデーションが適用されずに更新されてしまう問題が発生してしまいます。)

リスト6.39: セキュアパスワードの完全な実装 GREEN app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end

この時点で、テストは成功するはずです。

リスト6.40: GREEN
$ bundle exec rake test:models

6.3.4 ユーザーの作成と認証

以上でUserモデルの基本部分が完了しましたので、今度は7.1でユーザー情報表示ページを作成するときに備えて、データベースに新規ユーザーを1人作成しましょう。また、Userモデルにhas_secure_passwordを追加した効果についても (たとえばauthenticateメソッドの効果なども) 具体的に見ていきます。

ただしWebからのユーザー登録はまだできない (第7章で完成させます) ので、今回はRailsコンソールを使ってユーザーを手動で作成することにしましょう。6.1.3で説明したcreateを使いますが、後々実際のユーザーを作成する必要が出てくるので、今回はサンドボックス環境は使いません。したがって、今回作成したユーザーを保存すると、データベースに反映されます。それでは、まずrails consoleコマンドを実行してセッションを開始し、次に有効な名前・メールアドレス・パスワード・パスワード確認を渡してユーザーを作成してみましょう。

$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-09-11 14:26:42", updated_at: "2014-09-11 14:26:42",
password_digest: "$2a$10$sLcMI2f8VglgirzjSJOln.Fv9NdLMbqmR4rdTWIXY1G...">

うまくデータベースに保存されたかどうかを確認するためには、開発環境用のデータベースをDB Browser for SQLiteで開き、 usersテーブルの中身を見ます (6.9)20。もしCloud IDEを使っている場合は、データベースのファイルをダウンロードして開いてください (6.5)。このとき、先ほど定義したUserモデルの属性 (6.8) に対応したカラムがあることにも注目しておいてください

images/figures/sqlite_user_row_with_password_3rd_edition
図6.9 SQLiteデータベースdb/development.sqlite3に登録されたユーザーの行

コンソールに戻ってpassword_digest属性を参照してみると、リスト6.39has_secure_passwordの効果を確認できます。

>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"

これは、Userオブジェクトを作成したときに、"foobar"という文字列がハッシュ化された結果です。bcryptを使って生成されているので、この文字列から元々のパスワードを導出することは、コンピュータを使っても非現実的です21

また6.3.1で説明したように、has_secure_passwordをUserモデルに追加したことで、そのオブジェクト内でauthenticateメソッドが使えるようになっています。このメソッドは、引数に与えられた文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較します。試しに、先ほど作成したuserオブジェクトに対して間違ったパスワードを与えてみましょう。

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false

間違ったパスワードを与えた結果、user.authenticatefalseを返したことがわかります。次に、正しいパスワードを与えてみましょう。今度はauthenticateがそのユーザーオブジェクトを返すようになります。

>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-25 02:58:28", updated_at: "2014-07-25 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">

第8章では、このauthenticateメソッドを使ってログインする方法を解説します。なお、authenticateがUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrueであることが重要です。Userオブジェクトはnilでもfalseでもないので、いい感じに仕事をしてくれています22

>> !!user.authenticate("foobar")
=> true

6.4 最後に

この章では、ゼロからUserモデルを作成し、そこにname属性やemail属性、パスワード属性を加えました。また、それぞれの値を制限する多くの重要なバリデーションも追加しました。さらに、与えられたパスワードをセキュアに認証できる機能も実装しました。たった12行でここまでの機能が実装できたことは、(Railsの) 注目に値する点でもあります。

次の第7章では、ユーザーを作成するためのユーザー登録フォームを作成し、各ユーザーの情報を表示するためのページも作成します。第8章では、6.3の認証システムを利用して、ユーザーが実際にWebサイトにログインできるようにします。

Gitを使用している方は、しばらくコミットしていなかったのであれば、この時点でコミットしておくのがよいでしょう。

$ bundle exec rake test
$ git add -A
$ git commit -m "Make a basic User model (including secure passwords)"

次にmasterブランチにマージして、リモートにあるリポジトリに対してpushします。

$ git checkout master
$ git merge modeling-users
$ git push

なお、本番環境でUserモデルを使うためには、heroku runコマンドを使ってHeroku上でもマイグレーションを走らせる必要があります。

$ bundle exec rake test
$ git push heroku
$ heroku run rake db:migrate

うまくできたかどうかは、本番環境のコンソールに接続することで確認できます。

$ heroku run console --sandbox
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2014-08-29 03:27:50", updated_at: "2014-08-29 03:27:50",
password_digest: "$2a$10$IViF0Q5j3hsEVgHgrrKH3uDou86Ka2lEPz8zkwQopwj...">

6.4.1 本章のまとめ

  • マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
  • Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
  • Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
  • よくあるバリデーションには、存在性・長さ・フォーマットなどがある
  • 正規表現は謎めいて見えるが非常に強力である
  • データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
  • has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる

6.5 演習

: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルブックのすべての演習の解答が掲載されており、www.railstutorial.orgで本書を購入いただいた方には無料で配布しています (訳注: 解答は英語です)。

演習とチュートリアル本編の食い違いを避ける方法については、3.6のトピックブランチの演習に追加したメモをご覧ください。

  1. リスト6.31の、メールアドレスを小文字に変換するコードに対するテストを、リスト6.41に示されているように作成してください。このテストでは、reloadメソッドを使用してデータベースから値を再度読み込み、assert_equalメソッドを使用して同値であるかどうかをテストしてください。リスト6.41のテストが正しいかどうか検証するために、before_saveの行をコメントアウトするとテストが失敗し、元に戻すと成功することを確認してください。
  2. before_saveコールバック内でemail.downcase!と書き、email属性を直接変更してもよいことを、テストスイートを走らせて確認してください (リスト6.42のように書いてもよいことを、テストスイートを実行して確認してください。
  3. 6.2.4で説明したように、 リスト6.21のメールアドレスチェックする正規表現は、“foo@bar..com”のようにドットが連続した無効なメールアドレスを許容してしまいます。このメールアドレスをリスト6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト6.43に示したもう少し複雑な正規表現を使用して、このテストがパスするようにしてください。
リスト6.41: リスト6.31のメールアドレス小文字変換をテストする test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    duplicate_user.email = @user.email.upcase
    @user.save
    assert_not duplicate_user.valid?
  end

  test "email addresses should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
  end

  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end
end
リスト6.42: before_saveコールバックの別の実装 GREEN app/models/user.rb
class User < ActiveRecord::Base
  before_save { email.downcase! }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end
リスト6.43: 有効なメールアドレスかどうか (ドットが2つ以上連続するかどうか) を検証する正規表現 GREEN app/models/user.rb
class User < ActiveRecord::Base
  before_save { email.downcase! }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\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
  1. この名前の由来は “active record pattern” です。Martin Fowler著「エンタープライズ アプリケーションアーキテクチャパターン 」で特定および命名されました。
  2. 「エスキューエル」と発音しますが、「スィークゥエル」もよく使われます。
  3. メールアドレスをユーザー名にしたことで、ユーザー同士で通信できるように拡張できる可能性が開かれます (第10章)。
  4. tオブジェクトが具体的に何をしているのかを正確に知る必要はありませんので、どうか心配しないでください。抽象化レイヤの素晴らしい点は、それが何であるかを知る必要がないという点です。安心してtオブジェクトに仕事を任せればよいのです。
  5. 公式には「エスキューエライト (ess-cue-ell-ite)」と発音しますが、(本来は誤りとされている)「スィークゥエライト (sequel-ite)」もよく使われています。
  6. この唯一の例外が12.3.3に記されています。
  7. "2014-07-24 00:57:46"というタイムスタンプが気になった方もいると思いますが、著者はこの箇所を真夜中過ぎに書いたわけではありません。実はこのタイムスタンプは協定世界時 (UTC) に合わせてあります。これはグリニッジ標準時 (GMT) と同様、標準時間として使用されます。「NIST時刻と周波数FAQ」によると、問: 協定世界時 (Coordinated Universal Time) の略称がCUTではなくUTCなのはなぜですか 。答え: 協定世界時システムは、1970年に国際電気通信連合 (ITU) の技術専門家の国際諮問グループによって考案されました。このときITUは、混乱を最小限にとどめるために、略称を1つだけにしたいと考えました。このとき、英語式のCUTもフランス式のTUCも満場一致とならず、両者の妥協案としてUTCという略語が採用されました。
  8. 例外と例外ハンドリングは、ある意味でRubyの高度なテーマです。本書では例外についてこれ以上言及しません。しかし例外が重要なものであることも確かなので、12.4.1で推薦したRuby本で例外について詳しく学ぶことをおすすめします
  9. update_attributesメソッドはupdateメソッドのエイリアスですが、単一属性を変更するupdate_attributeメソッドとの違いを明確にするために、筆者は長いメソッド名の方を好んで使っています。
  10. 今後、コンソールコマンドの出力は、特に教育的効果が高いと思える場合 (ここでのUser.newの場合など) を除いて省略いたします。
  11. 驚いたことに、公式標準によると、たとえば"Michael Hartl"example.comのようなクォートとスペースを使用したメールアドレスも有効なのだそうです。まったく馬鹿げています。
  12. 6.1の正規表現の説明における「文字」は、実は「小文字のみ」が対象になっていることに注意してください。ただし、正規表現の末尾にiオプションを追加してあるので、大文字小文字が区別されずにマッチするようになっています。
  13. もしRubularのサービスが便利だと思ったら、素晴らしい功績を残した開発者であるMichael Lovittさんに報いるためにRubularへの寄付をお勧めします.。
  14. この節の冒頭で簡単に紹介したように、この目的に使用できる専用のテストデータベースdb/test.sqlite3があります。
  15. 技術的には、メールアドレスのうちドメイン名部分だけが (本当は) 大文字小文字を区別しません。foo@bar.comは、本来はFoo@bar.comとは別のアドレスです。ただし現実的には、about.comでも指摘されているように、メールアドレスの大文字小文字を区別することを前提にするのはまずい方法です。「メールアドレスの大文字小文字を区別すると、果てしない混乱と相互運用性の問題とひどい頭痛が発生する。メールアドレスの入力時に大文字小文字の区別を要求するのは賢い方法とは言えない。現実には、メールアドレスの大文字小文字の区別を強制するメールサービスやISPはめったに存在しない。メールアドレスのすべての文字を大文字にするなど、受信者のメールアドレスが誤って入力されていれば、メールは返送されるだけだ。」Riley Mosesによるご指摘に感謝いたします。
  16. もちろん、リスト6.2usersテーブル用のマイグレーションファイルを単に編集することも可能なのですが、その場合ロールバックが必要となり、マイグレーションが戻ってしまいます。データモデルの変更が必要になったらその都度マイグレーションを行うのがRails流です。
  17. 他にどんなコールバックがあるのか知りたい場合は、Rails APIのコールバック (英語) を読んでみてください。
  18. ここでいう「仮想的 (Virtual)」とは、Userモデルのオブジェクトからは存在しているように見えるが、データベースには対応するカラムが存在しない、という意味です。
  19. ハッシュ化されたパスワードは、暗号化されたパスワードとよく誤解されがちです。たとえば、(実は本書の第1版や第2版でも間違っていたのですが) has_secure_passwordソースコードでもこの手の間違いがあります。というのも、専門用語としての「暗号」というのは、設計上元に戻すことができることを指します (暗号化できるという文には、復号もできるというニュアンスが含まれます)。一方、「パスワードのハッシュ化」では元に戻せない (不可逆) という点が重要になります。したがって、 「計算量的に元のパスワードを復元するのは困難である」という点を強調するために、暗号化ではなくハッシュ化という用語を使っています。(この間違った用語について指摘してくれたAndy Philipsに感謝します。)
  20. もしうまくいかなくても、いつでもデータベースの中身をリセットできるので安心してください。リセットしたい場合は、以下の手順を踏んでください。

    1. まずはコンソールから脱出してください (Ctrl-C)
    2. 次に、コマンドラインから$ rm -f development.sqlite3を実行してデータベースの中身を削除してください(第7章でもっと便利なメソッドを紹介します)
    3. $ bundle exec rake db:migrateコマンドを実行して、もう一度マイグレーションを走らせてください
    4. 再度Railsコンソールを開き、コンソール上での作業をもう一度やり直してみてください
     
  21. 設計上、bcryptアルゴリズムではハッシュ化する前にソルトを追加しています。これにより、辞書攻撃 (Dictionary Attacks)レインボーテーブル攻撃 (Rainbow Table Attacks) といったタイプの攻撃を防ぐことができます。
  22. 4.2.3で、!! という式が対応する論理値オブジェクト (!!nil => false) に変換されていたことを思い出してください。
Railsチュートリアルは,Ruby/Rails のアジャイル開発を得意とする YassLab によって運営・保守されております.
継続的に良いコンテンツを提供する為に,電子書籍解説動画のご購入を検討して頂けると幸いです m(_ _)m
スポンサーシップや商用利用などに関するご相談がありましたら,お問い合わせページよりお気軽にご連絡ください :)