###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります14 。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります14 。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していない
という問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。
解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意
であるようにすれば解決します
#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要
があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順
に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的
な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性
を考えるとよいでしょう。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から
探すだけで済みます。
emailインデックスを追加すると、データモデリングの変更が必要
になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーター
を使ってマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。
####メールアドレスの一意性を強制するためのマイグレーション
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
# usersテーブルのemailカラムにインデックスを追加
end
end
上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制
できるようになります。
最後に、データベースをマイグレートします。
$ rails db:migrate
上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースを```ロックしてマイグレーションを妨げている可能性があります。)
この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です(リスト 6.30)
(実はこのデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなっていなかっただけでした)
####Userのデフォルトfixture
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう。
#######一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります14 。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していない
という問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。
解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意
であるようにすれば解決します
#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要
があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順
に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的
な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性
を考えるとよいでしょう。
索引のない本では、渡された言葉(例えば、"foobar")が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります(紙バージョンの全表スキャン)。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から
探すだけで済みます。
emailインデックスを追加すると、データモデリングの変更が必要
になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーター
を使ってマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。
####メールアドレスの一意性を強制するためのマイグレーション
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
# usersテーブルのemailカラムにインデックスを追加
end
end
上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制
できるようになります。
最後に、データベースをマイグレートします。
$ rails db:migrate
上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースを```ロックしてマイグレーションを妨げている可能性があります。)
この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
つまり、リスト 6.1でユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていないことが原因です(リスト 6.30)
(実はこのデータはいずれも有効ではありませんが、fixture内のサンプルデータはバリデーションを通っていなかったので今まで問題にはなっていなかっただけでした)
####Userのデフォルトfixture
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう(リスト 6.31)。
#######一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります14 。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
###一意性を検証する
メールアドレスの一意性を強制するために(ユーザー名として使うために)、validatesメソッドの:uniquenessオプションを使います。
ただしここで重大な警告があります。次の文面は流し読みせず、必ず注意深く読んでください。
まずは小さなテストから書いていきます。
モデルのテストではこれまで、主にUser.newを使ってきました。
このメソッドは単にメモリ上にRubyのオブジェクトを作るだけです。
しかし、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録
する必要があります14 。
そのため、まずは重複したメールアドレスからテスト
していきます。
####重複するメールアドレス拒否のテスト
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
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードでは、@userと同じメールアドレスのユーザーは作成できないことを、@user.dup
を使ってテストしています。
dupは、同じ属性を持つデータを複製
するためのメソッドです。
@userを保存した後では、複製されたユーザーのメールアドレスが既にデータベース内に存在するため、ユーザの作成は無効
になるはずです。
テストをパスさせるために、emailのバリデーションにuniqueness: true
というオプションを追加します。
###メールアドレスの一意性を検証する
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
実装の途中ですが、ここで1つ補足します。
通常、メールアドレスでは大文字小文字が区別されません
。
すなわち、foo@bar.comはFOO@BAR.COMやFoO@BAr.coMと書いても扱いは同じ
です。
したがって、メールアドレスの検証ではこのような場合も考慮する必要があります。
この性質のため、大文字を区別しないでテストすることが重要
になり、実際のテストコードは下のようにしなければなりません。
####大文字小文字を区別しない、一意性のテスト
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
test "email addresses should be unique" do
duplicate_user = @user.dup
#dup データを複製する
duplicate_user.email = @user.email.upcase
# 大文字にする
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
上のコードではStringのupcase
メソッドを使っています。
このテストは最初のメールアドレスの重複テストと同じことをしていますが、大文字に変換したメールアドレスを使っている点が異なります。
もしこのテストが少し抽象的すぎると感じるなら、Railsコンソールを起動して確認しましょう。
user = User.create(name: "Example User", email: "user@example.com")
(0.1ms) begin transaction
(0.1ms) SAVEPOINT active_record_1
User Create (3.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Example User"], ["email", "user@example.com"], ["created_at", "2021-09-25 13:46:01.403754"], ["updated_at", "2021-09-25 13:46:01.403754"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Example User", email: "user@example.com", created_at: "2021-09-25 13:46:01", updated_at: "2021-09-25 13:46:01">
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user = user.dup
=> #<User id: nil, name: "Example User", email: "user@example.com", created_at: nil, updated_at: nil>
>> duplicate_user.email = user.email.upcase
=> "USER@EXAMPLE.COM"
>> duplicate_user.valid?
=> true
現在の一意性検証では大文字小文字を区別しているため、``Pduplicate_user.valid?はtrueになります。 しかし、ここでは
false```になる必要があります。
幸い、:uniquenessでは:case_sensitiveという打ってつけのオプションが使えます。
####メールアドレスの大文字小文字を無視した一意性の検証
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false}
#???
end
ここで、trueをcase_sensitive: false
に置き換えただけであることに注目してください。
Railsはこの場合、:uniqueness
をtrueと判断します。
この時点で、アプリケーションは重要な警告と共にメールアドレスの一意性を強制
し、テストスイート
もパスするはずです。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 19358
Started with run options --seed 17050
16/16: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.38493s
16 tests, 35 assertions, 0 failures, 0 errors, 0 skips
しかし、依然としてここには1つの問題が残っています。
それはActive Recordはデータベースのレベルでは一意性を保証していない
という問題です。
具体的なシナリオを使ってその問題を説明します。
同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。
解決策の実装は簡単です。
実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。
具体的にはデータベース上のemailのカラムにインデックス(index)を追加し、そのインデックスが一意
であるようにすれば解決します
#####データベースのインデックス
データベースにカラムを作成するとき、そのカラムでレコードを検索する(find)必要
があるかどうかを考えることは重要です。
残念なことに、(インデックスなどの機能を持たない)素朴なデータモデルにおいてユーザーをメールアドレスで検索するには、データベースのひとりひとりのユーザーの行を端から順
に読み出し、そのemail属性と渡されたメールアドレスを比較するという非効率的
な方法しかありません。
emailカラムにインデックスを追加することで、この問題を解決することができます。
データベースのインデックスを理解するためには、本の索引との類似性
を考えるとよいでしょう。
索引のない本では、渡された言葉(例えば、"foobar")が出てくる箇所をすべて見つけるためには、ページを端から順にめくって最後まで探す必要があります。
しかし索引のある本であれば、"foobar"を含むすべてのページを索引の中から
探すだけで済みます。
emailインデックスを追加すると、データモデリングの変更が必要
になります。
今回の場合は、既に存在するモデルに構造を追加するので、次のようにmigrationジェネレーター
を使ってマイグレーションを直接作成する必要があります。
$ rails generate migration add_index_to_users_email
ユーザー用のマイグレーションと異なり、メールアドレスの一意性のマイグレーションは未定義になっています。
下のように定義を記述する必要があります。
####メールアドレスの一意性を強制するためのマイグレーション
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
# usersテーブルのemailカラムにインデックスを追加
end
end
上のコードでは、usersテーブルのemailカラムにインデックスを追加するためにadd_index
というRailsのメソッドを使っています。
インデックス自体は一意性を強制しませんが、オプションでunique: trueを指定することで強制
できるようになります。
最後に、データベースをマイグレートします。
$ rails db:migrate
上のマイグレーションが失敗した場合は、実行中のサンドボックスのコンソールセッションを終了していることを確認してください。
そのセッションがデータベースをロックしてマイグレーションを妨げている可能性があります。
この時点では、テストDB用のサンプルデータが含まれているfixtures内で一意性の制限が保たれていないため、テストは red になります。
ユーザー用のfixtureが自動的に生成されていましたが、ここのメールアドレスが一意になっていない
ことが原因です
####Userのデフォルトfixture
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
email: MyString
two:
name: MyString
email: MyString
また、このfixtureは第8章になるまで使わない予定なので、今のところはこれらのデータを削除しておき、ユーザー用のfixtureファイルを空にしておきましょう。
####空のfixtureファイル
#空にする
これで1つの問題が解決されましたが、メールアドレスの一意性を保証するためには、もう1つやらなければならないことがあります。
それは、いくつかのデータベースのアダプタが、常に大文字小文字を区別するインデックス を使っているとは限らない
問題への対処です。
この問題を避けるために、今回は「データベースに保存される直前にすべての文字列を小文字に変換する
」という対策を採ります。
これを実装するためにActive Recordのコールバック(callback)
メソッドを利用します。
このメソッドは、ある特定の時点で呼び出されるメソッドです。
今回の場合は、オブジェクトが保存される時点で処理を実行したいので、before_save
というコールバックを使います。
これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換
します 。
作成したコードを下示します。
####email属性を小文字に変換してメールアドレスの一意性を保証する
class User < ApplicationRecord
before_save { self.email = email.downcase }
# データベースに保存する前に処理をする。
# 入力されたメールアドレスを小文字にする。
# コールバックの一つ
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
コードはbefore_saveコールバックのブロックまでパスし、downcaseという文字列メソッドを用いてユーザーのメールアドレスの現在の値を小文字に変換
します。
リスト 6.32ではuniqueness制約をtrueに戻していることにご注目ください。
メールアドレスが小文字で統一されれば、大文字小文字を区別するマッチが問題なく
動作できるからです。実際、この手法によってデータベースインデックスを適用する際の問題を防止しています。
制約を元に戻すとテストが失敗しますが、前の形に戻すだけで簡単に修正できます。修正結果をリスト 6.33に改めて示します。
####元のメールuniquenessテストに戻す
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
#dup データを複製する
@user.save
assert_not duplicate_user.valid?
# 複製されたデータの存在は有効じゃないかを確認
end
end
self.email = self.email.downcase
Userモデルの中では右式のselfを省略できる
ので、今回は次のように書きました
(ちなみにこのselfは現在のユーザーを指します)
self.email = email.downcase
palindrome内でreverseメソッドを使っていたときも、同様のケースであったことを思い出してください。
そのときと同様で、左式ではselfを省略することはできません
。したがって、
email = email.downcase
と書くとうまく動きません。
これで、先に述べたアリスのシナリオはうまくいくようになります。
データベースは、最初のリクエストに基づいてユーザーのレコードを保存しますが、2度目の保存は一意性の制約に反するので拒否します。
email属性にインデックスを付与したことによって、メールアドレスからユーザーを引くときに全表スキャンを使わずに済むようになった。
###演習
1.リスト 6.34のように、メールアドレスを小文字にするテストをリスト 6.26に追加してみましょう。
ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。
リスト 6.34のテストがうまく動いているか確認するためにも、before_saveの行をコメントアウトして red になることを、また、コメントアウトを解除すると green になることを確認してみましょう。
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 saved as lower-case" do
mixed_case_email = "Foo@ExAMPle.CoM"
@user.email = mixed_case_email
#入力されるemail
@user.save
assert_equal mixed_case_email.downcase, @user.reload.email
# 小文字にしたemailと元のデータベースと等しいかを確かめる
end
end
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 7317
Started with run options --seed 16177
17/17: [============================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.07393s
17 tests, 36 assertions, 0 failures, 0 errors, 0 skips
比べる相手がないからテストは失敗しそう。
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 7451
Started with run options --seed 60508
ERROR["test_email_addresses_should_be_saved_as_lower-case", #<Minitest::Reporters::Suite:0x00007fc074cfef40 @name="UserTest">, 0.07783683099978589]
test_email_addresses_should_be_saved_as_lower-case#UserTest (0.08s)
ActiveRecord::RecordNotFound: ActiveRecord::RecordNotFound: Couldn't find User without an ID
test/models/user_test.rb:68:in `block in <class:UserTest>'
17/17: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.59611s
17 tests, 35 assertions, 0 failures, 1 errors, 0 skips
Couldn't find User without an ID
多分予想通りだと思う。
2.テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう。
ヒント: メソッドの末尾に!を付け足すことにより、email属性を直接変更できるようになります(リスト 6.35)。
class User < ApplicationRecord
before_save { email.downcase! }
# データベースに保存する前に処理をする。
# 入力されたメールアドレスを小文字にする。
validates :name, presence: true, length: { maximum: 50 }
#属性はname,属性の存在を検証、 最大50字まで
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
#最大255字まで
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
#???
end
ubuntu:~/environment/sample_app (modeling-users) $ rails t
Running via Spring preloader in process 8051
Started with run options --seed 55108
17/17: [============================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.56079s
17 tests, 36 assertions, 0 failures, 0 errors, 0 skips