ActiveRecord Normalization の使い方メモ
基本形
class User
normalizes :name, with: -> e { e.strip }
end
user = User.create!(name: " xxx ") # => #<User id: 1, name: "xxx">
user.name_before_type_cast # => "xxx"
User.where(name: " xxx ") # => #<ActiveRecord::Relation [#<User id: 1, name: "xxx">]>
where するときも渡した値が正規化されているのがわかる。
重ねて実行できる
class User
normalizes :name, with: -> e { e.next }
normalizes :name, with: -> e { e.next }
end
User.create!(name: "0").name # => "2"
とはいえ、実行順序に依存する書き方はやめたほうがよさそう。
nil なら空文字列にしたい場合に注意する点
class User
normalizes :name, with: -> e { e || "" }, apply_to_nil: true
end
user = User.create! # => #<User id: 1, name: nil>
user.name_before_type_cast # => nil
まず nil に対応するため apply_to_nil: true
を指定する。そうしておいてから User.create!
すると name は空文字列になる、はずが nil のままだった。
この場合 name: nil
をわざわざ書かないといけない。
user = User.create!(name: nil) # => #<User id: 2, name: "">
user.name_before_type_cast # => ""
または normalize_attribute(:name)
を呼ばないといけない。
user = User.new # => #<User id: nil, name: nil>
user.normalize_attribute(:name) # => nil
user.save!
user.reload # => #<User id: 3, name: "">
そのようにしないといけない理由は一応ドキュメントに
- 属性が割り当てられたり更新されたりしたときに適用される
と書いてあるからなんだけど、知らないと想定していた初期値が入らなくてわりとはまる。
before_validation で設定していたときのような安心感を得るのと代償に、かなり気持ち悪いコードを我慢できるなら before_validation
内で normalize_attribute(:name)
とする手もある。
where の条件も正規化されるはずでは?
before_validation で初期値を入れていたコードを、
class User
before_validation on: :create do
self.name ||= SecureRandom.hex
end
end
User.create!.name # => "b16a9658c3bd22538ad85dc92a0be801"
normalizes で書き直してしまうと、
class User
normalizes :name, with: -> e { e || SecureRandom.hex }, apply_to_nil: true
end
name に初期値が入るのは意図した通りだが、
User.create!(name: nil) # => #<User id: 1, name: "58c8186e2a08f58eec1bacfd41f31e43">
正規化はさらに where の条件にも適用されるので where(name: nil)
は where(name: SecureRandom.hex)
としたのと同じことになり、IS NULL
条件を書くのが難くなってしまう、ので、この面からしても apply_to_nil: true
の乱用はよくないんじゃないか。
──という主旨のことを書こうとしたら不具合なのか仕様なのかわからないけど、
ActiveRecord::VERSION::STRING # => "7.1.2"
の段階では、
User.where(name: nil).to_sql # => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"name\" IS NULL"
name: nil
はそのまま name is NULL
になる。
apply_to_nil が効いていないのか、where から来たものには適用しないよう意図的に避けているのかはわからない。
before_validation とどっちが先に呼ばれる?
class User
normalizes :name, with: -> e { e || "b" }, apply_to_nil: true
before_validation do
self.name ||= "a"
end
end
User.create!(name: nil) # => #<User id: 1, name: "b">
normalizes が先に呼ばれる。定義した順番は関係ない。
初期値問題と正規化の最適解 (before_validation と併用する)
class User
normalizes :name, with: -> e { e.strip }
before_validation do
self.name ||= " alice "
end
end
User.create!(name: nil) # => #<User id: 1, name: "alice">
normalizes で初期値を入れようとするとちぐはぐした感じになるのがわかった。そこで「初期値を入れる」と「正規化する」を完全に分離して考える。before_validation では nil なカラムに初期値を入れる。一方、normalizes では正規化に専念する。
前の検証で normalizes の方が先に呼ばれることがわかったが、それは apply_to_nil: true
を指定していたから。今回の場合は before_validation のあとに normalizes が呼ばれるので、初期値に対して正規化が行われる。
単体で利用する
class User
normalizes :name, with: -> e { e.downcase }
end
User.normalize_value_for(:name, "A") # => "a"
これをヘルパーとして乱用するのは気持ち悪い。あくまでテストしやすくするために用意されているのだと思われる。
登録したカラム名を確認する
class User
normalizes :a, with: -> e { e.strip }
normalizes :b, with: -> e { e.strip }
end
User.normalized_attributes # => #<Set: {:a, :b}>
まとめ
- 向いているもの
- 前後の空白除去
- 全角半角統一など
- where 時の副作用を意識する
- 条件の値まで正規化されて嬉しいなら問題ない
- 条件の値が正規化されて違和感がある場合、その normalizes の使用は間違っている
- アンチパターン
- nil なカラムに初期値を入れたいので
apply_to_nil: true
を指定する- 初期値は before_validation で入れるべし
- 初期値の指定と正規化は分担すべし
- nil なカラムに初期値を入れたいので
Discussion