🐢

Railsで変更前の値を使ってバリデーションしたいときは、nested attributes を使ってはいけない

2025/02/11に公開

これは何?

  • 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