はじめに
ActiveRecord のバリデーションで,数値が一定範囲に収まっていることを検証したいとする。
たとえば,Item クラスの number
カラムの値が 3 以上 9 以下であることを検証するのに,昔は
class Item < ApplicationRecord
validates :number,
numericality: {
greater_than_or_equal_to: 3,
less_than_or_equal_to: 9
}
end
などと書いた。
こんな長ったらしい書き方はしたくないよね。
それが Rails 6.1 からは
class Item < ApplicationRecord
validates :number, numericality: {in: 3..9}
end
と書けるようになった(はずだった)。
いやー,実に良いですな。
このことは,以下の記事でも紹介されている。
Rails ガイドの「numericality」のところにも書かれている:
ところが実際やってみると,検証がスルーされてしまう。
どうなってんの?
追記 2023-06-03
@jnchito さんのコメントですべて氷解。
numericality
で in
オプションが使えるようになったのは Rails 6.1 ではなく Rails 7.0 だった(正確には 7.0.0.alpha1 から)。
TechRacho さんの記事はバージョンに関しては誤り(翻訳元の記事がまちがっていr)。
Rails ガイドについては,上記のリンクは最新版についてのもので,現時点では Rails 7.0 について書かれている。
Rails 6.1 版は無料では見られないが,英語原文は見ることができ,ここには in
について(当然)書かれていない。
なお,この機能は activemodel のほうで実装されているのだった(もちろん activerecord で使える)。
再現コード
実験は,Rails を使わず ActiveRecord 単体で可能だ。
activerecord 6.1 と sqlite3 がインストールされた環境で以下のコードを実行してみてほしい。
gem "activerecord", "~> 6.1"
require "active_record"
require "pathname"
db_path = Pathname(__dir__) / "db.sqlite3"
db_path.delete if db_path.exist?
ActiveRecord::Base.establish_connection adapter: "sqlite3",
database: db_path,
pool: 5,
timeout: 5000
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class CreateItem < ActiveRecord::Migration[6.1]
create_table :items do |t|
t.integer :number
end
end
class Item < ApplicationRecord
validates :number, numericality: {in: 3..9}
end
item = Item.new number: 10
p item.valid?
# => true
p item.errors.full_messages
# => []
3..9
に入ってないといけないところへ 10
を与えているのに,検証にパスしている(valid になっている)。もちろんエラーメッセージも空。
なんでやねん。
実験(1):greater_than_or_equal_to などを使う
バリデーションの書き方を,従来のものに変えてみよう:
- validates :number, numericality: {in: 3..9}
+ validates :number, numericality: {
+ greater_than_or_equal_to: 3,
+ less_than_or_equal_to: 9
+ }
すると
p item.valid?
# => false
p item.errors.full_messages
# => ["Number must be less than or equal to 9"]
のように,ちゃんと検証できる。
実験(2):activerecord 7.0
activerecord のバージョンを変えてみたらどうか。7.0 にしてみよう。
-gem "activerecord", "~> 6.1"
+gem "activerecord", "~> 7.0"
-class CreateItem < ActiveRecord::Migration[6.1]
+class CreateItem < ActiveRecord::Migration[7.0]
※validates
の書き方は in
を使ったほうに戻しておく。以下,常に最初のコードに対する差分を提示することにする。
実験に使った環境では activerecord 7.0.5 がインストールされていたので,実験は 7.0.5 だ。
これだとエラーが出る:
/Users/XXXX/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/activemodel-7.0.5/lib/active_model/validations/numericality.rb:53:in `public_send': undefined method `in?' for 10:Integer (NoMethodError)
unless value.public_send(RANGE_CHECKS[option], option_value)
^^^^^^^^^^^^
10
に対し,in?
というメソッドを呼ぼうとして NoMethodError が出ている。
どういうこっちゃ?
追記 2023-06-03
activerecord 7.0 で numericality
の in
オプションを使うには,activesupport が必要だった。
最低限
require "active_support/core_ext/object/inclusion"
を追加してやれば,期待どおりに in
が使える。
つまり,
p item.valid?
# => false
p item.errors.full_messages
# => ["Number must be in 3..9"]
といった結果になる。
なお,"active_support/core_ext/object/inclusion"
が面倒なら
require "active_support/all"
とすればよい。
実験(3):in
を他のものに変えてみる
activerecord 6.1 で,なぜ in
が効かないのだろう。
もしかして,このオプション(in
)を知らないのでは?
試しに,(たわむれに)in
を honyarara
にしてみよう:
- validates :number, numericality: {in: 3..9}
+ validates :number, numericality: {honyarara: 3..9}
はい,
p item.valid?
# => true
p item.errors.full_messages
# => []
のように,バリデーションにパスしちゃいました!
NumericalityValidator はオプション名のチェックなんぞしていないのだ。
てことは,1 文字のタイポでも,「検証されるべきが検証されない」バグになるわけだ。モデルのテストを網羅的に書かないとヤベェぞ。
マジかよ?
less_than_or_equals_to
を less_then_or_equals_to
と書いたらアウトだぞ?
このことは,activerecord 7.0 でも同じだった。
in
についてはエラーが出たが,honyarara
とかだと出ない。
ドキュメント
本件についてどこかに何か書いてないかと思い,いろいろ見てみたが,探し方が悪いのか何も見つけられなかった。
どうすれば
結局,どう書けばいいのだろうか。
可能なら greater_than_or_equal_to
の類は使いたくないぞ。
どうやら
validates :number, inclusion: {in: 3..9}
でいいようだ。
NumericalityValidator とちゃうんかいっ!
今ひとつ釈然としないが,どう書けばよいかだけは分かった。
追記 2023-06-03
結論:
-
numericality
でin
オプションが使えるのは activemodel 7.0 から - activesupport も必要(Rails なら意識しなくてよい)
感想:
- 疲れた(何時間も溶かした)