🛤️

ActiveRecord Normalization の使い方メモ

2023/12/09に公開

基本形

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 で入れるべし
      • 初期値の指定と正規化は分担すべし

Discussion