🐢
Railsで変更前の値を使ってバリデーションしたいときは、nested attributes を使ってはいけない
これは何?
- Railsでデータの変更前と変更後の値を比較したバリデーションを書きたいときのハマりどころについて記載する
- 環境
Rails 8
結論
nested_attributes
を使わず、assign_attributes
を使うと意図通りに動く。
ユースケース
以下のモデルで、子モデルのAccount
に定義したhoge_validate
で変更前後のデータを見たいとき。
supplier.rb
class Supplier < ApplicationRecord
has_one :account
accepts_nested_attributes_for :account
end
account.rb
class Account < ApplicationRecord
validate :hoge_validate, on: :update
private
def hoge_validate
puts account_number_was
puts account_number
end
end
うまくいかないケース
nested_attributes
で更新すると、update
が走るのでバリデーションは起動するが、insert
をしているので変更前後の値がうまく読み取れない(どちらも変更前のデータを取得している)。
my-rails-sandbox(dev)> Supplier.take.update!({account_attributes: {account_number: 5}})
Supplier Load (1.4ms) SELECT "suppliers".* FROM "suppliers" LIMIT 1 /*application='MyRailsSandbox'*/
TRANSACTION (0.9ms) BEGIN /*application='MyRailsSandbox'*/
Account Load (2.6ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = 1 LIMIT 1 /*application='MyRailsSandbox'*/
2
2
Account Update (1.3ms) UPDATE "accounts" SET "supplier_id" = NULL, "updated_at" = '2025-02-11 05:58:11.857811' WHERE "accounts"."id" = 11 /*application='MyRailsSandbox'*/
Account Create (1.4ms) INSERT INTO "accounts" ("supplier_id", "account_number", "created_at", "updated_at") VALUES (1, '5', '2025-02-11 05:58:11.860894', '2025-02-11 05:58:11.860894') RETURNING "id" /*application='MyRailsSandbox'*/
TRANSACTION (8.8ms) COMMIT /*application='MyRailsSandbox'*/
うまくいくケース
子モデルをassign_attributes
して、親モデルをsave
するとupdate
のみ走るので、適切に変更前後の値が取得できる。
my-rails-sandbox(dev)> s = Supplier.take
Supplier Load (1.6ms) SELECT "suppliers".* FROM "suppliers" LIMIT 1 /*application='MyRailsSandbox'*/
=>
#<Supplier:0x00007f10bac18e88
...
my-rails-sandbox(dev)> s.account.assign_attributes({account_number: 10})
Account Load (2.6ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."supplier_id" = 1 LIMIT 1 /*application='MyRailsSandbox'*/
=> nil
my-rails-sandbox(dev)> s.save!
5
10
TRANSACTION (1.2ms) BEGIN /*application='MyRailsSandbox'*/
Account Update (2.7ms) UPDATE "accounts" SET "account_number" = '10', "updated_at" = '2025-02-11 06:03:31.625632' WHERE "accounts"."id" = 12 /*application='MyRailsSandbox'*/
TRANSACTION (8.8ms) COMMIT /*application='MyRailsSandbox'*/
まとめ
Rails 7
では、update => insert
ではなくdelete => insert
となっていた。いずれにしても予期しない挙動をしている。差分を利用したバリデーションをしたいときは、nested_attributes
を利用しないほうが安全に見える。
Discussion