Railsのvalidatesメソッドでallow_blankを利用するときに気をつけること
Railsのバリデーションには共通オプションの allow_blank
があります。
allow_blank
は属性値が blank?
の場合にバリデーションがパスされるものです。 validates
メソッドにこのオプションを利用した際に、意図しない結果が出ました。原因はオプションの扱いが不適切だったことです。
オプションの設定によってバリデーションが意図しない挙動をし、データ作成や画面操作にも影響があり原因調査が厄介でした。 そのため、注意点と原因をブログに残します。
原因を調査したRailsのバージョンは6.1.4.7です。
注意点
validatesでallow_blank
を使う際に気をつけるべきことは、allow_blank
をValidator内のオプションとして設定せず、Validator外に設定することです。
つまり、以下の例のようにValidator内に allow_blank
書くのではなく、
validates :description, length: { maximum: 32, allow_blank: true }
このようにValidator外に書きます。
validates :description, length: { maximum: 32 }, allow_blank: true
Validatorが2つ以上含む場合は特に注意が必要です。いずれかのValidator内に allow_blank
の設定が無い場合、バリデーションのスキップが出来ず意図しない結果となる可能性が高いです。
バリデーションのスキップが出来るケースと出来ないケースを見ていきます。
属性 description
の値をブランクにし、バリデーションの設定ごと(ケース1〜ケース5)の valid?
の結果を確認します。
ケース1:Validatorが1つの場合かつ lengthValidator外にallow_blank
を設定
validates :description, length: { maximum: 32 }, allow_blank: true
ケース2:Validatorが1つの場合かつ lengthValidator内にallow_blank
を設定
validates :description, length: { maximum: 32, allow_blank: true }
ケース3:Validatorが2つの場合かつ lengthValidator内にallow_blank
を設定
validates :description, length: { maximum: 32, allow_blank: true }, format: { with: /\\A(?!.*\\.\\.)[a-z0-9_.]\\z/, }
ケース4:Validatorが2つの場合かつ formatValidator内にallow_blank
を設定
validates :description, length: { maximum: 32 }, format: { allow_blank: true, with: /\\A(?!.*\\.\\.)[a-z0-9_.]\\z/, }
ケース5:Validatorが2つの場合かつ Validator外にallow_blank
を設定
validates :description, allow_blank: true, length: { maximum: 32, allow_blank: true }, format: { with: /\\A(?!.*\\.\\.)[a-z0-9_.]\\z/, }
これらのケースで自分が期待したことは、allow_blank
がバリデーションをスキップするので、valid?の結果が全てのケースでtrueになることでした。valid?を実行した結果は以下となります。
valid? | |
---|---|
ケース1 | true |
ケース2 | true |
ケース3 | false |
ケース4 | true |
ケース5 | true |
ケース3のみfalseとなりました。しかし、LengthValidatorにminimumを追加するとケース4でもfalseになります。このように、allow_blank
を設定する箇所によってバリデーションがスキップ出来ないケースがあります。
原因
allow_blank
の設定箇所によってvalid?
の結果が異なる原因を探るために、 validates
で設定したオプションの扱い方と allow_blank
のチェック方法についてRailsのコードを見てみます。
validates
で設定したオプションの扱い方
まずは、 validates
で設定したオプションをどのように扱っているのか見てみます。処理の概要は以下となります。
- validatesの引数は複数のvalidatorと複数のオプション(Validator外)が混合しているため、最初にValidatorとオプションに分類。
- 例えば、
length: { minimum: 8 }, allow_blank
の場合、Validatorはlength
、オプションはallow_blank
。
- 例えば、
- Validator外に設定されたオプションを各Validator内のオプションとして追加。
length : { minimum: 8 }
,allow_blank: true
の場合、LengthValidatorのオプションは、{ minimum: 8, allow_blank: true }
となる。
- Validator内に設定されたオプションはそのまま。他のValidatorに追加されない。
実際のコードは以下になります。一部コードの省略とコメントを追加しています。
def validates(*attributes) defaults = attributes.extract_options!.dup # Validator外で設定したオプション validations = defaults.slice!(*_validates_default_keys) # ValidatorとValidator内で設定したオプション # 省略 validations.each do |key, options| key = "#{key.to_s.camelize}Validator" # 省略 # Validator内とValidator外のオプションをマージ # validates_withでValidatorにレコードを渡す validates_with(validator, defaults.merge(_parse_validates_options(options))) end end
各Validatorごとにバリデーションを実行
次に valid?
を実行したときの挙動を見てみます。こちらの処理の概要は以下になります。
- レコードのバリデーション(
EachValidator#validate
)を実行。Validator共通の処理はここで行い、Validator固有の処理はvalidate_each
で実行。 validate
でValidatorのオプションに共通オプションallow_blank
が設定され、かつ値がブランクであった場合、validate_each
の実行はスキップされる。
実際のコードは以下になります。
def validate(record) attributes.each do |attribute| value = record.read_attribute_for_validation(attribute) # Validatorの共通処理。allow_blankがtrueで設定される&値がブランクであればバリデーションをスキップ。 next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) value = prepare_value_for_validation(value, record, attribute) validate_each(record, attribute, value) end end
まとめ
validates
で設定したオプションの扱い方と allow_blank
のチェック方法について調べたまとめは以下となります。
- Validator内に
allow_blank
の設定があればブランクのチェックを行い、チェックが通ればバリデーションの実行はスキップされる。 validates
で 全ての Validatorにallow_blank
を設定するには、Validator外にallow_blank
を設定するor 全てのValidator内にallow_blank
を設定する。
このことからケース3がfalseになった理由は下記となります。
FormatValidatorのオプションに allow_blank
が設定されてないため、バリデーションのスキップされず、FormatValidatorのバリデーションが実行される。 バリデーションのwith
オプションによるチェックは、値のブランクに無効であるため false
の結果となる。
最後に
allow_blank
は複数のValidatorに影響するオプションです。その点を踏まえれば、
Validator内に書くことはしないのでは?と思いました。ただ何となくオプションを使わないように気をつけたいです。