Railsではバリデーション(Validation)という仕組みがあります。
フォームなどでユーザーからの入力値をDBに保存する前にその値が正しいものかモデル層で(システムとして許可している値か)を検証する仕組みです。
バリデーションの基本的な流れ、バリデーションの定義とバリデーションのテスト方法、バリデーションのスキップなどのバリデーションの基本についてまとめました。
動作確認
- Rails 4.1
- ActiveRecord 4.1
- shoulda-matchers 2.6.3
目次
- 1. Railsでのバリデーションの流れ
- 2. バリデーションを定義する
- 3. バリデーションをスキップする
- 4. 条件付きバリデーションを定義する
- 5. バリデーションがvalidかinvalidか確認する
- 6. 独自のカスタムバリデーションを定義する
1. Railsでのバリデーションの流れ
Railsでのバリデーションの流れをステップバイステップで説明します。1. validates
を使い、モデルクラスに検証する条件を定義する。
# app/models/product.rb class Product < ActiveRecord::Base # 商品名は必須 validates :title, presence: true # 値段は数値で0以上 validates :price, numericality: { only_integer: true, greater_than_or_equal_to: 0 } end
2. コントローラーなどでモデルを保存/更新する。
# app/controllers/products_controlle.rb class ProductsController < ApplicationController def create # product_paramsにユーザーの入力値がハッシュ形式で入っている @product = Product.new(product_params) # 商品クラスのバリデーション定義に照らし合わせ、 # - バリデーションエラーが発生しなかった場合、DBにレコードが保存され、show画面に遷移する # - バリデーションエラーが発生した場合、DBにレコードは保存されず、new画面を再び表示する if @product.save redirect_to @product, notice: '商品を作成しました' else render :new end end end
3. バリデーションエラー時には、errors
にエラーメッセージが設定されるので、それを表示する
# app/views/products/_form.html.erb <%= form_for(@product) do |f| %> <!-- バリデーションエラー時のみ、@product.errors に値があるので、エラーメッセージを表示する --> <% if @product.errors.any? %> <div id="error_explanation"> <h2><%= @product.errors.count %>件のエラーが発生しました。</h2> <ul> <% @product.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> ... <% end %>
2. バリデーションを定義する
バリデーションの定義方法を説明します。また、
RSpec
と2.1. 存在チェック(presence)
presence
オプションにより、指定した属性が空でないことを検証する。
# 定義 validates :name, presence: true # 値が存在すれば検証成功 # テスト it { is_expected.to validate_presence_of(:name) }
boolean型のカラムの場合は、次のようにinclusion
を使います。(テストはinclusionの項目を参照)
# boolean型のカラムの場合の存在チェック validates :completed, inclusion: { in: [true, false] }
2.2. 一意性(ユニーク制約)のチェック(uniqueness)
uniqueness
オプションにより、指定した属性がユニークであることを検証する。
# 定義 validates email, uniqueness: true # 値がユニークであれば検証成功 # テスト it { is_expected.to validate_uniqueness_of(:email) }
注意点として、サーバー高負荷時などにユーザが2度登録ボタンを押してしまい、モデルでユニーク制約を記載していたのに同じ値がDBに登録されてしまうという問題が発生してしまいます。
そのため、モデルのバリデーションだけではなく、DBでもユニーク制約をつけておくべきです。
# マイグレーションファイルの作成 rails g migration add_index_to_users_email # マイグレーションファイル # db/migrate/YYYYMNDDHHMMSS_add_index_to_users_email.rb class AddIndexToUsersEmail < ActiveRecord::Migration def change add_index :users, :email, unique: true end end # マイグレート rake db:migarte
複数カラムでの一意性を検証したい場合には、scope
パラメータを指定します。
もちろん、DBとしても複数カラムで一意性制約を追加してください。
以下の例では、ユーザー単位に投稿(Article)のタイトルを一意にする検証を追加しています。
# 定義 class Article < ActiveRecord::Base belongs_to :user validates :title, uniqueness: { scope: [:user_id] } end # テスト it { is_expected.to validate_uniqueness_of(:title).scoped_to(:user_id) } # マイグレーション add_index :articles, [:title, :user_id], unique: true
メールアドレスの場合は、小文字に変換して一意性を保証すべきです。
# app/models/user.rb class User < ActiveRecord::Base before_save { self.email = email.downcase } end
2.3. 長さのチェック(length)
length
オプションにより、指定した値の長さを検証する。マルチバイト文字であっても1文字としてカウントします。
# 定義 class Person < ActiveRecord::Base validates :name, length: { minimum: 2 } # 値が「2文字以上」であれば有効 validates :bio, length: { maximum: 500 } # 値が「500文字以下」であらば有効 validates :password, length: { in: 6..20 } # 値が「6文字以上20文字以下」であれば有効 validates :registration_number, length: { is: 6 } # 値が「6文字のみ」有効 end # テスト it { is_expected.to ensure_length_of(:name).is_at_least(2) } it { is_expected.to ensure_length_of(:bio).is_at_most(500) } it { is_expected.to ensure_length_of(:password).is_at_least(6).is_at_most(20) } it { is_expected.to ensure_length_of(:registration_number).is_equal_to(6) }
2.4. フォーマットのチェック(format)
format
オプションにより、指定した値のフォーマットを検証する。※正規表現を検証するためにRubularで、テストしたい正規表現とサンプル文字列を入力することで正規表現にマッチするか調べられます。
# 定義 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i #メールアドレスフォーマットの検証(完璧な正規表現ではない) validates :email, format: { with: VALID_EMAIL_REGEX } # 特定の値が入ってほしくない場合は without オプションを利用 # validates :email, format: { without: <入ってほしくない値の正規表現> } # テスト it "valid emails" do valid_emails = %w( user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn ) is_expected.to allow_value(*valid_emails).for(:email) end it "invalid emails" do invalid_emails = %w( user@foo,com user_at_foo.org example.user@foo. foo@bar_baz.com foo@bar+baz.com foo@bar..com ) is_expected.not_to allow_value(*invalid_emails).for(:email) end
2.5. 数値の値チェック(numericality)
numericality
オプションにより、指定した値(数値)を検証する。
# 定義 class Player < ActiveRecord::Base validates :points, numericality: true # 数値か小数点のみ有効 validates :games_played, numericality: { only_integer: true } # 数値のみ有効 end # テスト it { is_expected.to validate_numericality_of(:points) } it { is_expected.to validate_numericality_of(:games_played).only_integer }
only_integer: true
をしたいした場合、数値の値自体の検証をすることができます。
- greater_than
- greater_than_or_equal_to
- equal_to
- less_than
- less_than_or_equal_to
- odd (奇数)
- even (偶数)
# 定義 # 数値であり、0以上の場合有効 validates :games_played, numericality: { only_integer: true, greater_than_or_equal_to: 0 } # テスト it { is_expected.to validate_numericality_of(:games_played) .only_integer.is_greater_than_or_equal_to(0) }
2.6. コレクションに含まれていることを検証(inclusion)
inclusion
オプションにより、inで指定したコレクションに含まれていることを検証する。
# 定義 validates :size, inclusion: { in: %w(small medium large) } # テスト it { is_expected.to ensure_inclusion_of(:size).in_array(%w(small medium large)) }
2.7. コレクションに含まれていないことを検証(exclusion)
exclusion
オプションにより、inで指定したコレクションに含まれていないことを検証する。
# 定義 validates :subdomain, exclusion: { in: %w(www us ca jp) } # テスト it { is_expected.to ensure_exclusion_of(:subdomain).in_array(%w(www us ca jp)) }
3. バリデーションをスキップする
次のメソッドはバリデーションを実行し、成功した場合のみ、DBに保存/更新します。- create
- create!
- save
- save!
- update
- update!
下記のメソッド群、もしくは、上記のメソッドにvalidate: false
を引数に追加することでバリデーションをスキップできます。
- decrement!
- decrement_counter
- increment!
- increment_counter
- toggle!
- touch
- update_all
- update_attribute
- update_column
- update_columns
- update_counters
# バリデーションがスキップされる save(validate: false)
4. 条件付きバリデーションを定義する
ある条件のときにだけバリデーションを実施したい場合に、条件付きバリデーションが使えます。条件付きバリデーションを使うためには、validatesメソッドに
if
かunless
オプションを指定して下さい。
class Order < ActiveRecord::Base validates :card_number, presence: true, if: :paid_with_card? def paid_with_card? payment_type == "card" end end
5. バリデーションがvalidかinvalidか確認する
valid?
とinvalid?
メソッドを使うことでバリデーションが「有効」か「無効」かをboolean型で取得することができます。このメソッドをコールした後に、バリデーションが失敗した場合、
errors
内にエラーメッセージが設定されます。
# バリデーション定義 # app/models/article.rb class Article < ActiveRecord::Base validates :title, presence: true end # 利用 ## valid? メソッド(true) Article.new(title: "タイトル").valid? # => true ## valid? メソッド(false) article = Article.new(title: nil) article.valid? # => false (errorsに値がエラーメッセージが設定される) article.errors.full_messages # => ["Title can't be blank"] ## invalid? メソッド(false) Article.new(title: "タイトル").invalid? # => false ## invalid? メソッド(true) article = Article.new(title: nil) article.invalid? # => true (errorsに値がエラーメッセージが設定される) article.errors.full_messages # => ["Title can't be blank"]
6. 独自のカスタムバリデーションを定義する
標準のバリデーションでは足りない場合に、独自のカスタムバリデーションを作成することができます。カスタムValidate
validate
メソッドを使うことで「独自のカスタムバリデーション」を作成できます。
validate
メソッドには「メソッド名のシンボル」を渡し、そのメソッド内でバリデーションを実装します。
バリデーションを実施するメソッド内では、errors
に値を設定することにより、バリデーションエラーになったということをActiveRecordに知らせます。
# app/models/invoice.rb class Invoice < ActiveRecord::Base validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value def expiration_date_cannot_be_in_the_past if expiration_date.present? && expiration_date < Date.today errors.add(:expiration_date, "can't be in the past") end end def discount_cannot_be_greater_than_total_value if discount > total_value errors.add(:discount, "can't be greater than total value") end end end
カスタムValidator
ActiveModel::Validator
を拡張することにより「カスタムValidator」を作成できます。
この継承したクラスは、validate
メソッドを実装しなければいけません。
複数のモデルで同じカスタムバリデーションを使いたいときに使うのが良いです。
# カスタムValidatorを作成 class MyValidator < ActiveModel::Validator def validate(record) unless record.name.starts_with? 'X' record.errors[:name] << 'Need a name starting with X please!' end end end # app/models/person.rb class Person # カスタムValidatorを追加 include ActiveModel::Validations validates_with MyValidator end
以上です。
参考文献
- Rails3 レシピブック 190の技
- Active Record Validations — Ruby on Rails Guides