boolean 型のカラムに presence のバリデーションをしようとして失敗した話
はじめに
class Wish < ApplicationRecord
validates :granted, presence: true
end
Rails の開発をしていて boolean 型のカラム(granted)に null が入らないよう、上記のようなバリデーションを設定していました。
しかし、これでは granted が false の場合、バリデーションエラーとなってしまいました。
ActiveRecord::RecordInvalid: バリデーションに失敗しました: Grantedを入力してください
今回の記事はこの問題の解説と解決方法をまとめたものです。
動作環境
- Ruby 3.2.2
- Rails 7.1.3.2
- PostgreSQL 14.12
presence
Rails ガイドには以下のように書かれていました。
このヘルパーは、指定された属性が空(empty)でないことを確認します。値が nil や空文字でない、つまり空でもなければホワイトスペースでもないことを確認するために、内部で
Object#blank?メソッドを使っています。(中略)
false.blank?は常に true なので、真偽値に対してこのメソッドを使うと正しい結果が得られません。
Rails ガイド -> Active Record バリデーション -> 2. バリデーションヘルパー -> 2.9presence
どうやら Object#blank? メソッドの返り値が true だとバリデーションに失敗するようですね。
( presence: true の場合)
というわけで Rails コンソールで確かめてみました。
[1] pry(main)> false.blank?
=> true
[2] pry(main)> nil.blank?
=> true
つまり、 false.blank? の返り値が true なので、 presence: true のバリデーションが失敗しているということですね。
これでは nil だけでなく false の値も許容しないバリデーションとなってしまい、意図しない挙動となってしまっています。
Object.blank? メソッド
このメソッドは Ruby の Object クラスには存在せず、Rails の Active Support による拡張機能のひとつです。
では false のクラスである FalseClass が継承しているクラスを ancestors メソッドで確認してみましょう。
[1] pry(main)> false.class
=> FalseClass
[2] pry(main)> FalseClass.ancestors
=> [ActiveSupport::ToJsonWithActiveSupportEncoder,
FalseClass,
JSON::Ext::Generator::GeneratorMethods::FalseClass,
MessagePack::CoreExt,
ActiveSupport::Dependencies::RequireDependency,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
PP::ObjectMixin,
ActiveSupport::Tryable,
JSON::Ext::Generator::GeneratorMethods::Object,
DEBUGGER__::TrapInterceptor,
Kernel,
BasicObject]
false は FalseClass のインスタンスであり、 Object クラスを継承しているので blank? メソッドが使用できるというわけですね。
ancestors メソッド
ancestors メソッドは親クラスとインクルードしているモジュールを配列にして返してくれる Module クラスのインスタンスメソッドです。
[1] pry(main)> FalseClass.class
=> Class
[2] pry(main)> Class.ancestors
=> [ActiveSupport::DescendantsTracker::ReloadedClassesFiltering,
Class,
Module,
Module::Concerning,
ActiveSupport::Dependencies::RequireDependency,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
PP::ObjectMixin,
ActiveSupport::Tryable,
JSON::Ext::Generator::GeneratorMethods::Object,
DEBUGGER__::TrapInterceptor,
Kernel,
FalseClass は Class クラスのインスタンスであり、 Class クラスは Module クラスを継承しているので、 ancestors メソッドが使用できるということです。
ちなみに、 class メソッドは Class クラスのメソッドではなく、 Object クラスのインスタンスメソッドです。
解決方法
こちらも Rails ガイドに記載されていました。
真偽値の存在をチェックしたい場合は、以下のいずれかを使う必要があります。
# 値は true か false でなければならない validates :boolean_field_name, inclusion: [true, false] # 値は nil であってはならない、すなわち true か false でなければならない validates :boolean_field_name, exclusion: [nil]これらのバリデーションのいずれかを使うことで、値が決して nil にならないようにできます。nil があると、ほとんどの場合 NULL 値になります。
Rails ガイド -> Active Record バリデーション -> 2. バリデーションヘルパー -> 2.9presence
inclusion
class Wish < ApplicationRecord
- validates :granted, presence: true
+ validates :granted, inclusion: [true, false]
end
inclusion を使用することで granted が true または false であることを検証します。
exclusion
class Wish < ApplicationRecord
- validates :granted, presence: true
+ validates :granted, exclusion: [nil]
end
exclusion は inclusion と逆で、 granted には nil が含まれていないことを検証します。
補足
PostgreSQL の boolean 型には true, false, unknown(SQL でいう null)の 3 つの状態しか保持しないようです。
なので上記の2種類のバリデーションのどちらでも対応できるということですね。
おわりに
今回のバリデーションの目的はデータベースに存在する boolean 型のカラムに null が入らないようにバリデーションを設定することです。
なので、個人的には exclusion の方が直感的だなと感じました。
お気づきの点があればコメントいただけると幸いです。
最後までお読みいただき、ありがとうございました。
参考記事
Discussion