kasei_sanのブログ

かせいさんのIT系のおぼえがきです。胡乱の方はnoteとtwitterへ

Rails ガイド Active Record バリデーション

Active Record バリデーション | Rails ガイド

クライアント側でのバリデーションは扱いやすく便利ですが、一般に単独では信頼性が不足します。JavaScriptを使用してバリデーションを実装する場合、ユーザーがJavaScriptをオフにしてしまえばバイパスされてしまいます。ただし、他の方法と併用するのであれば、クライアント側でのバリデーションはユーザーに即座にフィードバックを返すための便利な方法となるでしょう。
コントローラレベルのバリデーションは一度はやってみたくなるものですが、たいてい手に負えなくなり、テストも保守も困難になりがちです。アプリケーションの寿命を永らえ、保守作業を苦痛なものにしないようにするためには、コントローラのコード量は可能な限り減らすべきです。

耳が痛い

Railsチームは、ほとんどの場合モデルレベルのバリデーションが最も適切であると考えています。

バリデーションが実行されるメソッド

以下のメソッドではバリデーションがトリガされ、オブジェクトが有効な場合にのみデータベースに保存されます。

  • create
  • create!
  • save
  • save!
  • update
  • update!
  • new では、バリデーションは実行されない

 

save(validate: false) で、バリデーションをスキップできる!

#valid?, #invalid?

  • #valid? : バリデーションがOKなら true
  • #invalid? : バリデーションがNGなら true

バリデーション後は、errors.messages を参照すると、発生したエラーにアクセスできる

user = User.new
user.valid? # => false
user.errors.messages # => {name:["空欄にはできません"]}

バリデーションヘルパ

validates_associated

関連づけられているモデルのバリデーションも同時に実行する

class Library < ActiveRecord::Base
  has_many :books
  validates_associated :books
end

confirmation

  • 2つのテキストフィールドの値が一致しているか確認
  • パスワードとかメールアドレスとか

form画面で、以下のように、email と、email_confirmation を用意する

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>
validates :email, confirmation: true
# confirmation は、email_confirmation が空の場合チェックをしないので、
# email_confirmation 側で必須チェックを行う必要がある
# allow_blank: false じゃダメなのかな?
validates :email_confirmation, presence: true

そのほか

# acceptance
#
# チェックボックスがONになっているか確認
# 利用条項を確認しました。とかそんなのに使う
validates :terms_of_service, acceptance: true

# inclusion, exclusion
#
# 特定の値であることを確認(ラジオボタンの選択値のチェックとか)
validates :size, inclusion: { in: %w(small medium large) }
# 特定の値でないことを確認
validates :subdomain, exclusion: { in: %w(www us ca jp) }

# format
#
# 正規表現と一致するか確認
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/ }

# length
#
# 文字長を確認
validates :name, length: { minimum: 2 }           # 最小
validates :bio, length: { maximum: 500 }          # 最大
validates :password, length: { in: 6..20 }        # 範囲
validates :registration_number, length: { is: 6 } # 固定

# numericality
#
# 数値のみであるか確認
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true } # 小数点禁止
# 他のオプション
# - greater_than, greater_than_or_equal_to
# - equal_to
# - less_than, less_than_or_equal_to
# - even, odd

# presence, absence
#
# 必須チェック(#blank? メソッドを呼ぶ)
validates :name, :login, :email, presence: true
# その逆(#present? メソッドを使う)
validates :name, :login, :email, absence: true

# false.blank? は true なので、真偽値のチェックは inclusion を使う
validates :field_name, inclusion: { in: [true, false] }

# uniqueness
#
# ユニークチェック
# より厳密にやるには、DB側でもuniq制約をつけること
validates :email, uniqueness: true
# 複数カラムの組み合わせでユニークチェック
validates :email, uniqueness: { scope: :year }
# 大文字小文字は区別しない
validates :email, uniqueness: { case_sensitive: false }

validates_each

複数のカラムに対して、独自のバリデーションを実行する

class Person < ActiveRecord::Base
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
  end
end

共通のバリデーションオプション

  • allow_nil : nilの場合バリデーションを行わない
  • allow_blank : blankの場合バリデーションを行わない ruby # 何も入力していない時に、必須チェックエラーと、文字数チェックエラー両方を出さないようにする # 必須だけれど、何も入力しない時は、文字数チェックを実施しない validates :name, persence:true, length: { maximum: 256, allow_blank: true }
  • message : エラーメッセージ
  • strict : バリデーション失敗時例外を発生させる

on オプション

バリデーションの実行タイミングを指定

  • on: :create : 新規作成時のみ
  • on: :update : 更新時のみ

独自のコンテキストを設定することも可能

validates :username, presence: true, on: :registration

呼び出し

@user.save(context: :registration)
@user.valid?(:registration)

条件付きバリデーション

if, unless オプションの条件を満たす時にバリデーションを実行する

# メソッド呼び出し
validates :card_number, presence: true, if: :paid_with_card?

def paid_with_card?
  payment_type == "card"
end

# eval
validates :surname, presence: true, if: "name.nil?"

# proc
validates :password, confirmation: true,
    unless: ->(a){ a.password.blank? }

# with_optionsでグループ化
with_options if: :is_admin? do |admin|
  admin.validates :password, length: { minimum: 10 }
  admin.validates :email, presence: true
end

# 複数条件指定
validates :mouse, presence: true,
                    if: [:retail?, :desktop?]
                    unless: Proc.new { |c| c.trackpad.present? }

カスタムバリデータ

# ActiveModel::Validator を継承。#validate を実装
class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.starts_with? 'X'
      record.errors[:name] << '名前はXで始まる必要があります'
    end
  end
end
 
class Person
  include ActiveModel::Validations
  validates_with MyValidator # validates_with で呼び出す
end

個別の属性を検証する

# ActiveModel::EachValidator を継承。#validate_each を実装する
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "は正しいメールアドレスではありません")
    end
  end
end

class Person < ActiveRecord::Base
  validates :email, presence: true, email: true
end

#errors

  • エラー情報を格納
  • キー : 属性名、値 : エラー文字列の配列
# エラーメッセージの追加
errors.add(:mail, 'メールアドレスが不正')
errors[:mail] = 'メールアドレスが不正'
errors[:base] # 個別の属性にかかわらない、オブジェクト全体のエラーを持つ
errors.clear  # エラーメッセージのクリア

参考