🔍

boolean 型のカラムに presence のバリデーションをしようとして失敗した話

2024/07/28に公開

はじめに

class Wish < ApplicationRecord
  validates :granted, presence: true
end

Rails の開発をしていて boolean 型のカラム(granted)に null が入らないよう、上記のようなバリデーションを設定していました。
しかし、これでは grantedfalse の場合、バリデーションエラーとなってしまいました。

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.9 presence

どうやら 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 による拡張機能のひとつです。

https://railsguides.jp/active_support_core_extensions.html#blank-questionmarkとpresent-questionmark

では 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]

falseFalseClass のインスタンスであり、 Object クラスを継承しているので blank? メソッドが使用できるというわけですね。

ancestors メソッド

ancestors メソッドは親クラスとインクルードしているモジュールを配列にして返してくれる Module クラスのインスタンスメソッドです。

https://docs.ruby-lang.org/ja/3.2/method/Module/i/ancestors.html

[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,

FalseClassClass クラスのインスタンスであり、 Class クラスは Module クラスを継承しているので、 ancestors メソッドが使用できるということです。
ちなみに、 class メソッドは Class クラスのメソッドではなく、 Object クラスのインスタンスメソッドです。

https://docs.ruby-lang.org/ja/3.2/method/Object/i/class.html

解決方法

こちらも 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.9 presence

inclusion

class Wish < ApplicationRecord
- validates :granted, presence: true
+ validates :granted, inclusion: [true, false]
end

inclusion を使用することで grantedtrue または false であることを検証します。

https://railsguides.jp/active_record_validations.html#inclusion

exclusion

class Wish < ApplicationRecord
- validates :granted, presence: true
+ validates :granted, exclusion: [nil]
end

exclusioninclusion と逆で、 granted には nil含まれていないことを検証します。

https://railsguides.jp/active_record_validations.html#exclusion

補足

PostgreSQL の boolean 型には true, false, unknown(SQL でいう null)の 3 つの状態しか保持しないようです。
なので上記の2種類のバリデーションのどちらでも対応できるということですね。

https://www.postgresql.org/docs/14/datatype-boolean.html

おわりに

今回のバリデーションの目的はデータベースに存在する boolean 型のカラムに null が入らないようにバリデーションを設定することです。
なので、個人的には exclusion の方が直感的だなと感じました。

お気づきの点があればコメントいただけると幸いです。
最後までお読みいただき、ありがとうございました。

参考記事

https://railsguides.jp/active_record_validations.html#presence
https://api.rubyonrails.org/v7.1/classes/Object.html#method-i-blank-3F
https://docs.ruby-lang.org/ja/3.2/class/Object.html
https://thoughtbot.com/blog/how-to-validate-the-presence-of-a-boolean-field-in-a-rails-model

Discussion