5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

validates :number, numericality: {in: 3..9} なんて使えないぞ?

Last updated at Posted at 2023-06-02

はじめに

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 さんのコメントですべて氷解。

numericalityin オプションが使えるようになったのは 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 で numericalityin オプションを使うには,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)を知らないのでは?

試しに,(たわむれに)inhonyarara にしてみよう:

-  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_toless_then_or_equals_to と書いたらアウトだぞ?

このことは,activerecord 7.0 でも同じだった。
in についてはエラーが出たが,honyarara とかだと出ない。

ドキュメント

本件についてどこかに何か書いてないかと思い,いろいろ見てみたが,探し方が悪いのか何も見つけられなかった。

どうすれば

結局,どう書けばいいのだろうか。
可能なら greater_than_or_equal_to の類は使いたくないぞ。

どうやら

  validates :number, inclusion: {in: 3..9}

でいいようだ。
NumericalityValidator とちゃうんかいっ!

今ひとつ釈然としないが,どう書けばよいかだけは分かった。

追記 2023-06-03

結論:

  • numericalityin オプションが使えるのは activemodel 7.0 から
  • activesupport も必要(Rails なら意識しなくてよい)

感想:

  • 疲れた(何時間も溶かした)
5
2
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?