メールアドレスはユーザーごとに一意であり、重複してはならない。
既存のユーザーと同じメールアドレスを持つユーザーは無効となるように、テストを書く。
test "email addresses should be unique" do
duplicate_user = @user.dup
@user.save
assert_not duplicate_user.valid?
end
dupメソッドは、@userインスタンスをコピーする。
@userが保存された後は、duplicate_userは無効でなければならない。
emailのバリデーションにuniqunessオプションを追加して、重複できないようにする。
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
###メールアドレスの大文字小文字
ところで、メールアドレスは大文字小文字を区別しないらしい。
つまり、foo@bar.comとFOO@BAR.COMとFoO@BAr.coMは同じである。
そこで、重複するユーザーのメールアドレスは、既存ユーザーのメールアドレスを大文字にして入れる。
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
メールアドレスの文字列を大文字にするために、upcaseメソッドを使っている。
現在のバリデーションでは、メールアドレスの大文字小文字を区別しているので、テストは失敗する。
@user.emailとduplicate_user.email(= @user.email.upcase)は別物と認識されるので、duplicate_userが有効となるからである。
メールアドレスの大文字小文字を区別しないようにするためには、uniquenessオプションにcase_sensitive: falseを設定する。
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
テストをして、成功することを確認する。
##データベースレベルでの一意性の検証とインデックス
ユーザー登録ボタンを2回クリックしたりすると、データベースに同じユーザーが二つ作成されてしまうことがあるらしい。
たまに「注文ボタンは一回だけ押してください」とか、「ボタンをクリックした後、ページの読み込みが遅くてもそのままお待ちください」とか書いてあることがあるが、それのことである。
これを解決するために、Userモデルのemailカラムにインデックスを付け、その一意性を設定する。
インデックスは本来、データベースの検索を高速化するためのものである。
Userモデルを変更するため、マイグレーションファイルを作成し、インデックスを追加する。
$ rails generate migration add_index_to_users_email
class AddIndexToUsersEmail < ActiveRecord::Migration[5.0]
def change
add_index :users, :email, unique: true
end
end
uniqueオプションをtrueとすることで、インデックスの一意性が保たれる。
ところで、バリデーションでは"uniqueness"と名詞だったのに、インデックスでは"unique"と形容詞なのはなぜ?
マイグレーションを実行する。
rails db:migrate
ここで、テスト用のユーザーデータを入れておくfixtureファイルに、同じメールアドレスが設定されたユーザーが存在しているために、テストが失敗する。
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
このfixtureファイルは、Userモデルを生成した際にできたものである。
これを空にしておき、テストが成功することを確認する。
# 空にする (既存のコードは削除する)
メモ:なぜかここでエラーが出た。
ActiveRecord::PendingMigrationError (
Migrations are pending. To resolve this issue, run:
bin/rake db:migrate RAILS_ENV=development
):
インデックス用のマイグレーションファイルを削除して、db:migrateを実行。
マイグレーションファイルを作り直してdb:migrateを再び実行すると直った。
##もう一つの問題
さらにもう一つ問題があるらしい。
「いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない問題への対処です。例えば、Foo@ExAMPle.Comとfoo@example.comが別々の文字列だと解釈してしまうデータベースがあります」
とのこと。
データベースのアダプタとやらがイマイチよく分からないが、とにかくこれを解決するため、データベースにメールアドレスを保存する際には小文字に変換して保存するようにする。
before_saveメソッドを使う。
class User < ApplicationRecord
before_save { self.email = self.email.downcase }
validates :name, presence: true, length: { maximum: 50 }
.
.
.
end
メールアドレスを小文字化して代入し直す。
selfは保存されるUserインスタンスを指す。
self.email = self.email.downcase
は、右側のselfを省略して
self.email = email.downcase
とも書ける。
!を使って次のように書くと、email属性を直接変更できるようである。
email.downcase!
###小文字化に対するテスト
メールアドレスが保存される際に、小文字化されているかをテストする。
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
注:最後のassert_equalで、なぜかreloadメソッドを使用している。
@userは保存された後なので、reloadをしても特に意味はないはずである。
実際にコンソールで確認したが、@user.emailと@user.reload.emailは同じ値を返し、reloadを除いてもテストに問題はなかった。