基本用語
一意の
「他に同じものがない」という意味。
スキーマ (schema)
データベースの構造のこと。
Railsではdb/schema.rbでデータベースの構造(スキーマ)を追跡出来る。
Active Record
データベースとやりとりをするデフォルトのRailsライブラリのこと。
ダックタイピング(duck typing)
Rubyの性質として、そのクラスを詳しく知らなくてもなんとなくオブジェクトをどう扱えば良いかわかる、という性質がある。
これをダックタイピング (duck typing) と呼び、よく次のような格言で言い表されている(「
もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」)。
fixtures
事前に用意したテストデータを読み込み、常にDBの内容を一定に保つための仕組みのこと。
主にテストに使う場合のデータ登録用に使う。
ちなみにfixture内のサンプルデータはバリデーションを通らない。
ハッシュ化
ハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにする処理のこと。
文法
モデルを生成する
コントローラ名には複数形を使い、モデル名には単数形を用いる(コントローラはUsersでモデルはUser)。
rails generate model User name:string email:string
Railsコンソールをサンドボックスモードで起動する
コンソールをサンドボックスで起動すると、そのセッションで行ったデータベースへの変更をコンソールの終了時にすべて “ロールバック” (取り消し) してくれる。
rails console --sandbox
ユーザーオブジェクトを検索する
Active Recordには、オブジェクトを検索するための方法がいくつもある。
ここでは、User.findにユーザーのidを渡し、Active Recordはそのidのユーザーを返す。
# id=1のユーザーを検索する
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
# 特定の属性でユーザーを検索する
>> User.find_by(email: "mhartl@example.com")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
# ユーザー名でユーザーを検索する
>> User.find_by_name("Michael Hartl")
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Michael Hartl"], ["LIMIT", 1]]
=> #<User id: 2, name: "Michael Hartl", email: "mhartl@example.com", created_at: "2018-04-20 05:17:11", updated_at: "2018-04-20 05:17:11">
また、User.allではデータベースのすべてのUserオブジェクトが返ってくる。
返ってきたオブジェクトのクラスはActiveRecord::Relation(各オブジェクトを配列として効率的にまとめてくれるクラス)となっている。
>> User.all
=> #<ActiveRecord::Relation [#<User id: 1, name: "Michael Hartl",
email: "mhartl@example.com", created_at: "2016-05-23 19:05:58",
updated_at: "2016-05-23 19:05:58">, #<User id: 2, name: "A Nother",
email: "another@example.org", created_at: "2016-05-23 19:18:46",
updated_at: "2016-05-23 19:18:46">]>
あるテストだけを走らせる
# 統合テストだけを走らせる
rails test:integration
# モデルに関するテストだけを走らせる
rails test:models
errorsオブジェクト
エラーメッセージを表示する。
>> user.errors.full_messages
=> ["Name can't be blank"]
# ハッシュ形式でエラーが取得できる
u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.save
(0.1ms) SAVEPOINT active_record_1
(0.1ms) ROLLBACK TO SAVEPOINT active_record_1
=> false
>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]
長さを検証する
test "name should not be too long" do
@user.name = "a" * 51
assert_not @user.valid?
end
dupメソッド
同じ属性を持つデータを複製するためのメソッド。
duplicate_user = @user.dup
既に存在するモデルに構造を追加する
rails generate migration add_password_digest_to_users password_digest:string
reloadメソッド
データベースの情報を元にオブジェクトを再読み込みする。
>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"
Railsとデータベース
Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル(Model)と呼ぶ。
Railsでは、データを永続化するデフォルトの解決策として、データベースを使ってデータを長期間保存する。
また、データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ばれている。
Active Recordは、データオブジェクトの作成/保存/検索のためのメソッドを持っている。
これらのメソッドを使うのに、リレーショナルデータベースで使うSQL (Structured Query Language)2 を意識する必要はない。
さらに、Railsのマイグレーション (Migration)という機能は、データの定義をRubyで記述することができ、SQLのDDL (Data Definition Language) を新たに学ぶ必要がない。
つまりRailsは、データベースの細部をほぼ完全に隠蔽し、切り離してくれる。
マイグレーション
マイグレーションは、データベースの構造をインクリメンタルに変更する手段を提供する。
それにより、要求が変更された場合にデータモデルを適合させることができる。
rails generate model User name:string email:string
invoke active_record
create db/migrate/20160523010738_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
このUserモデルの場合、マイグレーションはモデル生成スクリプトによって自動的に作られる。
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
マイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加される。
マイグレーション自体は、データベースに与える変更を定義したchangeメソッドの集まり。
上記の場合、changeメソッドはcreate_tableというRailsのメソッドを呼び、ユーザーを保存するためのテーブルをデータベースに作成する。
create_tableメソッドはブロック変数を1つ持つブロックを受け取る。
ここでは (“table”の頭文字を取って) tで表している。
そのブロックの中でcreate_tableメソッドはtオブジェクトを使って、nameとemailカラムをデータベースに作る。
モデル名は単数形(User)、テーブル名は複数形(users)で書く。
これはRailsで用いられる言葉の慣習を反映している。
ブロックの最後の行t.timestampsは特別なコマンドで、created_atとupdated_atという2つの「マジックカラム(Magic Columns)」を作成する。
生成されたUserのデータモデル
マイグレーションの適用
マイグレーションは、b:migrateコマンドを使って実行することができる。
rails db:migrate
初めてdb:migrateが実行されると、SQLite5データベースの実体であるdb/development.sqlite3という名前のファイルが生成される。
development.sqlite3ファイルを開くためのDB Browser for SQLiteというツールを使うと、データベースの構造を見ることができる。
検証
オブジェクトが有効か調べる
※データベースにデータがあるかどうかは有効性には関係ない。
user.valid?
実例としては、有効なUserかどうかをテストしたりすることが出来る。
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
属性を更新する
saveメソッド
>> user # userオブジェクトが持つ情報のおさらい
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 19:05:58", updated_at: "2016-05-23 19:05:58">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true
update_attributesメソッド
属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行う (保存に成功した場合はtrueを返します)。
ただし、検証に1つでも失敗すると、 update_attributesの呼び出しは失敗する。
>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"
update_attributeメソッド
特定の属性のみを更新したい場合は、次のようにupdate_attributeを使う。
このupdate_attributeには、検証を回避するといった効果もある。
>> user.update_attribute(:name, "El Duderino")
=> true
>> user.name
=> "El Duderino"
存在性を検証する
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
assert_notメソッド
ここでは、Userオブジェクトが有効でなくなったことを確認している。
assert_not @user.valid?
メールアドレスの検証
# 有効なアドレスの検証
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
# 無効なアドレスの検証
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
メールアドレスのフォーマット
このオプションは引数に正規表現 (Regular Expression) (regexとも呼ばれます) を取る。
validates :email, format: { with: /<regular expression>/ }
ここでは、実用的で、堅牢であることが実戦で保証されている正規表現を採用する。
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
この正規表現を理解するために、お手頃なサイズに分割して表にまとめた。
正規表現 | 意味 |
---|---|
/\A[\w+-.]+@[a-z\d-.]+.[a-z]+\z/i | (完全な正規表現) |
/ | 正規表現の開始を示す |
\A | 文字列の先頭 |
[\w+-.]+ | 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す |
@ | アットマーク |
[a-z\d-.]+ | 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す |
. | ドット |
[a-z]+ | 英小文字を少なくとも1文字以上繰り返す |
\z | 文字列の末尾 |
/ | 正規表現の終わりを示す |
i | 大文字小文字を無視するオプション |
メールの正規表現を分解した結果
メールアドレスのフォーマットの例
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX }
end
一意性を検証する
メールアドレスの一意性を強制するために (ユーザー名として使うために)、validatesメソッドの:uniqueオプションを使う。
まずは小さなテストから書いていく。
モデルのテストではこれまで、主にUser.newを使ってきた。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけ。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要がある。
そのため、まずは重複したメールアドレスからテストしていく。
テストの例
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
dupは、同じ属性を持つデータを複製するためのメソッド。
通常、メールアドレスでは大文字小文字が区別されない(foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ)。
したがって、メールアドレスの検証ではこのような場合も考慮する必要がある。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効になるはず。
uniqueness: true
uniqueness: true、 一意性を強制する。
case_sensitiveオプションにfalseを指定することで、大文字小文字の差を無視することが可能。
# 一意性を強制する
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
# 一意性を強制する + 大文字小文字の差を無視する
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
データベースのレベルで一意性を保証する
Active Recordはデータベースのレベルでは一意性を保証していないため、以下のような問題が起こる(トラフィックが多いときにこのような問題が発生する可能性がある)。
- アリスはサンプルアプリケーションにユーザー登録する。メールアドレスはalice@wonderland.com。
- アリスは誤って “Submit” を素早く2回クリックしてしまう。そのためリクエストが2つ連続で送信される。
- 次のようなことが順に発生する。リクエスト1は、検証にパスするユーザーをメモリー上に作成する。リクエスト2でも同じことが起こる。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存される。
- この結果、一意性の検証が行われているにもかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまう。
データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要があるかどうかを考えることは重要。
例えば、以下のマイグレーションによって作成されたemail属性の場合、送信されたものと一致するメールアドレスのユーザーのレコードをデータベースの中から探しだす必要がある。
残念なことに、(インデックスなどの機能を持たない) 素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的な方法しかない。
つまり、例えばデータベース上の最後のユーザを探す場合でも、すべてのユーザーを最初から順に一人ずつ探していくことになる。
これは、データベースの世界では全表スキャン(Full-table Scan)として知られており、数千のユーザーがいる実際のサイトでは極めて不都合になる。
emailカラムにインデックスを追加することで、この問題を解決することができる。
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
(usersテーブルを作るための) Userモデルのマイグレーション
add_indexメソッドとインデックスの一意性の強制
migrationファイルを作成する。
rails generate migration add_index_to_users_email
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
def change
add_index :users, :email, unique: true
end
end
usersテーブルのemailカラムにインデックスを追加するためにadd_indexというRailsのメソッドを使っている。
インデックス自体は一意性を強制しないが、オプションでunique: trueを指定することで強制できるようになる。
データベースに保存される直前にすべての文字列を小文字に変換する
いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らないため、「データベースに保存される直前にすべての文字列を小文字に変換する」という対策を採る。
コールバック(callback)メソッド
コールバックメソッドは、ある特定の時点で呼び出されるメソッドのこと。
before_saveコールバック
今回の場合は、オブジェクトが保存される時点で処理を実行したいため、before_saveというコールバックを使う。
class User < ApplicationRecord
before_save { self.email = email.downcase } # 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
メールアドレスの小文字化のテスト
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
「データベースのレベルで一意性を保証する」のまとめ
データベースは、最初のリクエストに基づいてユーザーのレコードを保存するが、2度目の保存は一意性の制約に反するので拒否する(Railsのログにエラーが出力されるが、害は生じない)。
さらに、インデックスをemail属性に追加したことで、多数のデータがあるときの検索効率も向上させた。
これは、email属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになったため。
セキュアなパスワード
セキュアパスワードという手法では、各ユーザーにパスワードとパスワードの確認を入力させ、それを (そのままではなく) ハッシュ化したものをデータベースに保存する。
has_secure_passwordメソッド
このメソッドは、Userモデルで次のように呼び出せる。class User < ApplicationRecord
.
.
.
has_secure_password
end
このメソッドを追加すると、次のような機能が使えるようになる。
- セキュアにハッシュ化したパスワードを、データベース内のpassword_digestという属性に保存できるようになる。
- 2つのペアの仮想的な属性 (passwordとpassword_confirmation) が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される 。
- authenticateメソッドが使えるようになる (引数の文字列がパスワードと一致するとUserオブジェクトを、間違っているとfalseを返すメソッド) 。
has_secure_password機能を使えるようにするには、モデル内にpassword_digestという属性が含めることが条件。
rails generate migration add_password_digest_to_users password_digest:string
また、has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になる。
source 'https://rubygems.org'
gem 'rails', '5.1.4'
gem 'bcrypt', '3.1.11'
.
.
.
has_secure_passwordには、仮想的なpassword属性とpassword_confirmation属性に対してバリデーションをする機能も(強制的に)追加されているため、テストの@user変数にパスワードとパスワード確認の値をセットする必要がある。
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
パスワードの最小文字数の設定
多重代入(Multiple Assignment)
パスワードとパスワード確認に対して同時に代入をしている。
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
has_secure_passwordメソッドは存在性のバリデーションもしてくれるが、これは新しくレコードが追加されたときだけに適用される性質を持っている。
したがって、例えばユーザーが ’ ’ (6文字分の空白スペース) といった文字列をパスワード欄に入力して更新しようとすると、バリデーションが適用されずに更新されてしまう問題が発生してしまう。
そのため、存在性のバリデーションも一緒に追加する。
class User < ApplicationRecord
.
.
.
validates :password, presence: true, length: { minimum: 6 }
end
authenticateメソッド
authenticateメソッドは、引数に渡された文字列 (パスワード) をハッシュ化した値と、データベース内にあるpassword_digestカラムの値を比較する。
>> user.authenticate("foobaz")
false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2016-05-23 20:36:46", updated_at: "2016-05-23 20:36:46",
password_digest: "$2a$10$xxucoRlMp06RLJSfWpZ8hO8Dt9AZXlGRi3usP3njQg3...">
なお、authenticateがUserオブジェクトを返すことは重要ではなく、返ってきた値の論理値がtrueであることが重要。
>> !!user.authenticate("foobar") # !!でそのオブジェクトが対応する論理値オブジェクトに変換できる
=> true