Userモデルのバリデーションを解説【Ruby on Rails】

## どんな値も取れるname属性・email属性は問題！
例えば`name`は空であってはならず、`email`はメールアドレスのフォーマットに従う必要がある。さらに、メールアドレスをユーザーがログインするときの一意のユーザー名として使おうとしているため、メールアドレスがデータベース内で重複することのないようにする必要もある。

属性値には、何らかの制約を与える必要がある。Active Recordでは**検証（Validation)** という機能を通して、こういった制約を課すことができるようになっている。

よく使われるのは
* 存在（preference）の検証
* 長さ（length）の検証
* フォーマット（format）の検証
* 一意性（uniqueness)の検証
* 確認（confirmation）の検証

### 検証はテスト駆動開発にぴったりの機能！
`setup`メソッド内の処理は、各テストが走る直前に実行される。`@user`はインスタンス変数だが、setupメソッド内で宣言しておけば、全てのテスト内でこのインスタンス変数が使えるようになる。
したがって、`valid?`メソッドを使ってUserオブジェクトの有効性をテストすることができる。
```rb: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
```
### 存在を検証する
これは単に、「渡された属性が存在すること」を検証する。
まず、テストを追加する。`@user`変数の`name`属性に対して空白の文字列をセットする。そして、`assert_not`メソッドを使ってUserオブジェクトが有効でなくなったことを確認する
```ruby:test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
  test "email should be present" do
    @user.name = "  "
    assert_not @user.valid?
  end
end
```
この状態では、テストはRED（エラー）になる。
name属性の存在を検証するには、`validate`メソッドに`presence: true`という引数を与えて使う。
```ruby:app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
end
```
上記を設定した状態で、Userオブジェクトをデータベースに保存しようとすると自動的に失敗する。そのため、この設定をした上で`rails test:models`を実行するとエラーがなくなる。
### 長さを検証する
ユーザーの名前はWebサイト上に載ったりもするので、名前の長さにも制限を与える必要がある。
ここでは、50を上限とする。また、ほとんどのデータベースでは文字列の上限を255としているため、それに合わせて255文字を上限とする。

以下は、nameとemailの長さを検証するテスト
```ruby:test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
  .
  .
  .
  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"
    asseert_not @user.valid?
  end
end
```
この時点で、テストはエラーになっているが、バリデーションの`:maximum`パラメータの`:length`を設定することで、長さを強制することができる。
```rb:app/models/user.rb
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end
```
### フォーマットの検証
`email`属性に関しては適切なフォーマットであるかを検証する必要がある。

メールアドレスのバリデーションは扱いが難しく、エラーが難しい部分なので、有効なメールアドレスと無効なメールアドレスをいくつか用意して、バリデーション内のエラーを検知していく。

具体的には、「user@example,com」のような無効メールアドレスが弾かれて、「user@example.com」のような有効メールアドレスが通ることを確認しながら、バリデーションを実施する。
`test "email validation should accept valid addresses"`は有効メールアドレスが通るかを確認するテストであり、`test "email validation should reject invalid addresses"`で無効アドレスを弾けるかをテストしている。

```rb: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

  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

この状態で、`rails test`を実行すると、エラーになることを確認したら、`email`の`validate`を設定する。

メールアドレスのフォーマットを検証するために、次のように`format`というオプションを使う。
```ruby
validates :email, format: {with: /<regular expression>/ }`
```
`regular expression`の箇所には、正規表現を入れる。メールアドレス標準を定める実践的でシンプルな正規表現は、
```
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
```
上記正規表現を適用した、バリデーションが以下になる。
```ruby:app/models/user.rb
class User < ApplicationRecord
  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
```
適用後、`rails test`を実行するとエラーは無くなっている。
### 一意性を検証する
メールアドレスの一意性を検証するために、`validates`メソッドの`:uniqueness`オプションを使う。

まずは、テストを書く。`User.new`は単にメモリ上にRubyオブジェクトを作るだけであり、一意性のテストのためには、メモリ上だけではなく、実際にレコードをデータベースに登録する必要がある。
```ruby:test/models/user_test.rb
  test "email addresses should be unique" do
    duplicate_user = @user.dup
    @user.save
    assert_not duplicate_user.valid?
  end
```
ここでは、`dup`メソッドを使って、`setup`メソッドで設定した`@user`と同じ属性を持つデータを複製し、それが保存できるかをテストしている。

上記テストを通過するには、`email`のバリデーションに`uniquess: true`というオプションを追加する。
```ruby:app/models/user.rb
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 },
                    uniqueness: true
end
```
また、通常メールアドレスは大文字小文字を区別しない。つまり、`user@example.com`は`USER@EXAMPLE.COM`や`User@EXample.coM`と書いても扱いは同じ。したがって、このような場合も検証では考慮する必要がある。

まずは、テストに追記する。
```ruby:test/models/user_test.rb
  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`メソッドを使って、大文字に変換したメールアドレスが登録されるのを防ぐことができるかをテストする。

`validate`では、`uniqueness`に対して`:case_sensitive`というオプションで`false`を設定するだけで良い。
```ruby:app/models/user.rb
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 },
                    uniqueness: { case_sensitive: false }
end
```
最後に、未だに残っている問題として、**Active Record**はデータベースのレベルでは一意性を保証していない**という問題。
もし、ユーザーが送信ボタンを２連続で押してしまうなどした場合、
1. 1つ目のリクエストにより検証にパスするユーザーがメモリ上に作られる
1. 2つ目のリクエストにより検証にパスするユーザーがメモリ上に作られる
1. リクエスト1のユーザーが保存される
1. リクエスト2のユーザーが保存される
このように一意性の検証をしているにも関わらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまう。

解決策としては、データベース上のemailカラムに**index**を追加し、そのインデックスが一意であるようにすれば解決する。

emailインデックスを追加すると、データモデリングの変更が必要になる。
まずは、マイグレーションファイルを作成する。
```bash:ターミナル
$ rails generate migration add_index_to_users_email
```
その後、作成されたマイグレーションファイルを編集する。
モデルにカラムを追加する解説は[こちら]()。
```ruby:dp/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, :unique: true
  end
end
```
`users`テーブルの`email`カラムにインデックスを使うために、`add_index`メソッドを使っており、さらに一意性を付与するために、オプションで`unique: ture`を指定している。

最後に、データベースにマイグレート
```bash:ターミナル
$ rails db:migrate
```
`/test/fixtures/users.yml`内で一意性の制限が保たれていない場合は、テストを実行してもエラーになる。今回、エラーになって、これまでのテストでエラーになっていなにのは、fixture内のサンプルデータはバリデーションを通っていなかったため。

最後の最後に、メールアドレスの一意性を保つためにやるべきことがある。それはいくつかのデータベースのアダプタが、常に小文字と大文字を区別するインデックスを使ってるとは限らない問題への対処。
例えば、User@ExampLe.CoMとuser@example.comが別々の文字列だと解釈してしまうデータベースがある。

対処法としては「データベースに保存される直前に全ての文字列を小文字に変換する」という対策を採る。これを実装するために、ActiveRecordの**コールバック(callback)** メソッドを利用する。このメソッドは、ある特定の時点で呼び出されるメソッド。

オブジェクトが保存される時点で処理を実行するには、`before_save`というコールバックを使う。これを使って、ユーザーをデータベースに保存する前にemail属性を強制的に小文字に変換する。
```ruby:app/models/user.rb
class User < ApplicationRecord
  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
```
## セキュアなパスワードを追加する
セキュアなパスワードを実現するには、ユーザーにパスワードとパスワードの確認を入力させ、それを**ハッシュ化**したものをデータベースに保存する。


ハッシュ化とは、簡単に言うとハッシュ関数を使い入力されたデータを元に戻せない不可逆なデータにする処理を指す。

このハッシュ化されたパスワードは、ログイン機構でユーザーを**認証**する際に利用する。
### ハッシュ化されたパスワード
セキュアなパスワードの実装は、`has_secure_password`というRailsのメソッドを呼び出すだけでほとんど終わってします。
```rb
class User < ApplicationRecord
  .
  .
  has_secure_password
end
```
このメソッドにより以下のような機能が使えるようになる。
* セキュア化したパスワードを、データベース内の`password_digest`という属性に保存できるようになる。
* 2つのペアの仮想的な属性（`password`と`password_confirmation`)が使えるようになる。また、存在性と値が一致するかどうかのバリデーションも追加される。
* `authenticate`メソッドを使えるようになる（引数の文字列とパスワードと一致するとUserオブジェクトを、間違っていると`false`を返す)

この`has_secure_password`機能を使えるようにするにはモデル内に`password_digest`という属性が含まれている必要がある。

以下のコマンドを実行して、Userモデルに`password_digest`属性を追加する。さらにデータベースに対してマイグレーションを実行する。
```
$ rails generate migration add_password_digest_to_users password_digest:string
$ rails db:migrate
```
`has_secure_password`を使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になる。`bcrypt`を使うためには、bcrypt gemを`Gemfile`に追加する。
```Gemfile
source 'https://rubygems.org'

gem 'rails',          '6.0.4'
gem 'bcrypt',         '3.1.13'
gem 'bootstrap-sass', '3.4.1'
```
次に`$ bundle install`をターミナルで実行する。

Userモデルに`password_digest`属性を追加すれば、`has_secure_pssword`が使えるようになる。
```ruby:app/models/user.rb
class User < ApplicationRecord
  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
```
このままでは、テストが失敗してしまうので、`test/models/user_test.rb`の`setup`メソッド内の内容を修正する。
```ruby: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文字）を検証するテストを追記する。
```ruby:test/models/user_test.rb
.
.
.
  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
```
## Userモデルを作ってみる
これまでの設定を終えて、Userモデルのインスタンスは次のように作成できる。`create`は`new`と`save`を同時に行ってくれるメソッド。
```ruby:ターミナル
$ rails c
>> User.create(name: "Taro Yamada", email: "taro@yamada.com",
?>             password: "password", password_confirmation: "password")
```
`has_secure_password`をUserモデルに追加したことにより、`authenticate`メソッドが使えるようになる。このメソッドは、引数の文字列をハッシュ化した値と、データベースにある`password_digest'の値（パスワードがハッシュ化された値）を比較し、異なれば`false`、同じであればUserオブジェクトを返す。正しいバスワードを入れた際に`true`が返るようにするには、`!!user.authenticate("password")`とすればいい。