はじめに
第6回目となる今回は、前回作成したUserモデルにバリデーションを付けていきます。
バリデーションとは、日本語では『検証』と訳されますが、モデルの保存条件のようなもので、例えばname
はnil
じゃダメ、とかemail
は@
が含まれていないとダメ、とかそういうやつです。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のだけやりたい場合はこちらからダウンロードしてください。
Validationをつけてみる
早速Validationを付けてみます。いよいよModelファイルをいじる時がきました。
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
Validationはvalidates [attribute_name], [validations]
の形式で定義することができます。
一つの属性に対して複数のValidationを一気に定義していますね。
どんなValidationが定義されているのか紹介していきます!
presence
presence
は『存在性』のValidationです。presence: true
なので『存在しなければならない』ことを検証します。
ユーザー情報としてname
やemail
が不足しているのはおかしいですよね。
なのでname
とemail
両方にPresence validationを与えています。
動作を確認してみましょう。Rails consoleで確認していくのでまずはコンテナ&Rails consoleの起動から。
$ docker-compose up -d
$ docker-compose exec web ash
# rails c
> user = User.new
=> #<User:0x0000558587d7e8c8
id: nil,
name: nil,
email: nil,
created_at: nil,
updated_at: nil>
> user.save
(0.4ms) BEGIN
User Exists? (5.5ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1 [["LIMIT", 1]]
(0.4ms) ROLLBACK
=> false
おっと、save
メソッドでfalse
が返却されていますね。前回も少しお話しましたが、save
メソッドはデータのDB保存に失敗する場合(validationで引っかかる場合)、false
を返却するようになっています。
エラーの内容はuser.errors
に格納されます。中でもuser.errors.full_messages
を見れば、ユーザー向けのエラーメッセージが格納されているのでエラー理由が一目瞭然です。
> user.errors.full_messages
=> ["Nameを入力してください", "Emailを入力してください", "Emailは不正な値です"]
『を入力してください』がPresence validationに違反した場合のエラーメッセージです。
属性の日本語化
...
validationは日本語化されていますが属性は英語になっていますね...
それもそのはず。属性の表現の仕方を定義していないのですから!
ということで初期設定の時にi18n化対応したのと同じように、localesファイルを編集して『Name』を『お名前』、『Email』を『メールアドレス』と日本語化してあげましょう。
ja:
activerecord:
+ attributes:
+ user:
+ name: "お名前"
+ email: "メールアドレス"
errors:
...
属性の名称はactiverecord.attributes.[model_name].[attribute_name]
で定義します。
一度Rails consoleをリロードして、もう一度エラーを起こして確認してみましょう!
> reload!
Reloading...
=> true
> user = User.new
=> #<User:0x0000558587e99de8
id: nil,
name: nil,
email: nil,
created_at: nil,
updated_at: nil>
> user.save
(0.5ms) BEGIN
User Exists? (1.7ms) SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1 [["LIMIT", 1]]
(0.3ms) ROLLBACK
=> false
> user.errors.full_messages
=> ["お名前を入力してください", "メールアドレスを入力してください", "メールアドレスは不正な値です"]
属性も日本語化されたエラーメッセージに変わりました!
length
length
は属性値の長さを検証するvalidationです。
maximum
で最大文字数(最大桁数)、minimum
で最小文字数(最小桁数)、in
で最小と最大の範囲、is
で特定の文字数(桁数)を検証します。
今回は、name
には最大50文字、email
には最大255文字のvalidationを定義しているので、エラーの確認をするために51文字のnameと256文字のemailを持つUserモデルをsaveしてみましょう。
> user = User.new
=> #<User:0x0000558586601da8
id: nil,
name: nil,
email: nil,
created_at: nil,
updated_at: nil>
> user.name = "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
> user.email = "b" * 245 + "@sample.com"
=> "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"
> user.save
(0.3ms) BEGIN
User Exists? (2.7ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"], ["LIMIT", 1]]
(0.3ms) ROLLBACK
=> false
> user.errors.full_messages
=> ["お名前は50文字以内で入力してください", "メールアドレスは255文字以内で入力してください"]
文字数について検証してくれていることが確認できました!
format
format
は正規表現とマッチするかを検証するvalidationです。
今回のケースでは、正規表現として/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
を与えています。これはメールアドレスの正規表現として一般的なもので、簡単に言えば「【何か文字列】@【何か文字列】.【何か文字列】」を表しています。
正規表現の表現方法については今回は端折ります。例えば「Ruby 正規表現の使い方 - Qiita」などを参考に勉強してみてください。常に知っておく必要はあんまりないと思いますが、必要になったときに調べながらでも書けるようになっているのが望ましいでしょう。
では、早速このフォーマットバリデーションが正しく動作するかを確認します。例えば、「@」を抜いた「taro.com」なんていかがでしょうか?絶対メールアドレスじゃないのでちゃんとエラーになってほしいですよね。
> user = User.new(name: "taro", email: "taro.com")
=> #<User:0x00005585870da1f8
id: nil,
name: "taro",
email: "taro.com",
created_at: nil,
updated_at: nil>
> user.save
(0.8ms) BEGIN
User Exists? (2.6ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "taro.com"], ["LIMIT", 1]]
(0.8ms) ROLLBACK
=> false
> user.errors.full_messages
=> ["メールアドレスは不正な値です"]
メールアドレスのフォーマットエラーになりました。
uniqueness
uniqueness
は一意性を検証するvalidationです。
case_sensitive
をfalse
に設定すると大文字小文字の区別をしないで一意性を検証するようになります。email
で「taro@sample.com
」と「TARO@sample.com
」は同じメールアドレスですのでこのオプションを付与してます。
では検証してみましょう。
> User.create(name: "taro", email: "taro@sample.com")
(0.7ms) BEGIN
User Exists? (2.8ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "taro@sample.com"], ["LIMIT", 1]]
User Create (4.9ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-03-09 13:52:38.337106"], ["updated_at", "2020-03-09 13:52:38.337106"]]
(2.1ms) COMMIT
=> #<User:0x000055858729ad58
id: 1,
name: "taro",
email: "taro@sample.com",
created_at: Mon, 09 Mar 2020 13:52:38 JST +09:00,
updated_at: Mon, 09 Mar 2020 13:52:38 JST +09:00>
> user = User.create(name: "taro", email: "TARO@sample.com")
(0.5ms) BEGIN
User Exists? (1.9ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2 [["email", "TARO@sample.com"], ["LIMIT", 1]]
(0.4ms) ROLLBACK
=> #<User:0x00005585873922d8
id: nil,
name: "taro",
email: "TARO@sample.com",
created_at: nil,
updated_at: nil>
id
がふられていなかったり、created_at
、updated_at
のnil
であることからモデルオブジェクトの作成には成功していますがDB保存はできていないことがわかります。
> user.errors.full_messages
=> ["メールアドレスはすでに存在します"]
emailの一意性チェックでエラーになったことがわかります。また、大文字小文字を区別せずに一意性チェックをしてくれていることもわかりました。
DBの制約
ここまでモデル側、つまりアプリ側にvalidationをかけてきました。
このような制約はDB側でもかけることができますし、その方が安全だ!という考え方もあります。
Railsでは、デザインパターンとしてActiveRecordを採用しています。ActiveRecordでは「制約をかけるのはモデルの仕事」とされているため、上のようにモデル側で制約をかけています。
こうすることで、制約の内容が変わったとしてもDB側の設定を変えることなくアプリ側だけの改修で柔軟に対応をすることができます。
一方で一意性に関しては、複数のアプリが並列で処理を行う構成を考えるとDB側で制御されている方がよいとされています。
今回は一意性(uniqueness)に関して、DB側にも制約をかけます。
まず、マイグレーションファイルを生成します。
> quit
# rails g migration add_index_to_user
Running via Spring preloader in process 194
invoke active_record
create db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb
これでほとんど空のマイグレーションファイルが生成されますので、中身を書いていきます。
class AddIndexToUser < ActiveRecord::Migration[6.0]
def change
+ add_index :users, :email, unique: true
end
end
uniqueness
の制約はadd_index [table (model)], [column (attribute)], unique: true
で設定します。
ではマイグレーションファイルを適用していきましょー。
# rails db:migrate
== 20200130053807 AddIndexToUser: migrating ===================================
-- add_index(:users, :email, {:unique=>true})
-> 0.0942s
== 20200130053807 AddIndexToUser: migrated (0.0947s) ==========================
DBの一意性チェックは利用するDBによりますが大文字小文字を区別してしまう可能性があります。そのため、モデル側でDBに保存する前に強制的にemailを小文字化する処理を入れます。
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
user.save
をするときに、RailsではCallbacksといいますがシーケンシャルな処理が存在します。まずvalidation
が実行されsave
が実行されcommit
が実行されるといった流れです。
before_save
はその名前から分かるとおり、validation
が通った後、save
が始まる前に処理を挟み込むことを意味しています。before_save
の後の{}の中身が処理になりますが、email.downscale
で現在のemail
を小文字化して再度self.email
、つまり自分の属性値に代入しています。
さて、ここまでやるとDBでも一意制約を入れることができている状態になります。
一度モデル側のuniqueness validationを外しておき、DBだけで一意制約を担保できるか確認してみましょう。
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 }
+ format: { with: VALID_EMAIL_REGEX }
end
format: { with: VALID_EMAIL_REGEX }
が削除で追加になってるよ?と思うかもしれませんが、一番後ろの,
を消さないと定義が終わっていないものと見做されてエラーになるので、こんな感じの表現に...(わかりにくくてすみません!,
消して欲しいだけです!)
またコンソールから確認してみます。
# rails c
> user = User.create(name: "taro", email: "TARO@sample.com")
(0.3ms) BEGIN
User Create (8.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-03-09 14:00:36.861653"], ["updated_at", "2020-03-09 14:00:36.861653"]]
(0.2ms) ROLLBACK
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_email"
DETAIL: Key (email)=(taro@sample.com) already exists.
from /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:672:in `exec_params'
Caused by PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_users_on_email"
DETAIL: Key (email)=(taro@sample.com) already exists.
from /usr/local/bundle/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/postgresql_adapter.rb:672:in `exec_params'
先ほどのuniqueness
のときとは違う挙動をとっていることがわかります。
さらに、DETAIL
というところでメールアドレスの重複をDBが検知していることがわかりますね。
さて、DB側の制約でも一意性を担保できることを確認できたので、モデルのコメントアウトを元に戻しておきます。
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 }
+ format: { with: VALID_EMAIL_REGEX },
+ uniqueness: { case_sensitive: false }
end
後片付け
じゃあ、またデータ消しておきましょう。
> quit
# exit
$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset
$ docker-compose down
まとめ
今回は、Modelにvalidationを付与してみました。
ふんふん。ちゃんとvalidationをつけれましたね。
validationは他にもいろいろあります。
Active Record バリデーション - Railsガイド
自分のアプリケーションに合致するバリデーションを見つけましょう!
次回は、Userモデルにセキュアなパスワードを付与して行こうと思います。
パスワードを平文で持つのはやっぱりNG。Railsは簡単にセキュアなパスワードを実装できるんです!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.7 - Secure password - - Qiita