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内に書くことはしないのでは?と思いました。ただ何となくオプションを使わないように気をつけたいです。