rails / rails

Ruby on Rails
https://rubyonrails.org
MIT License
55.6k stars 21.55k forks source link

saved_changes from ActiveRecord::Dirty does not include some changes on nested attributes #47521

Open Tao-Galasse opened 1 year ago

Tao-Galasse commented 1 year ago

Steps to reproduce

I have those two classes (same example than the has_one association documentation from the Active Record Associations guide) :

class Supplier < ApplicationRecord
  has_one :account, inverse_of: :supplier
end

class Account < ApplicationRecord
  belongs_to :supplier, inverse_of: :account
  accepts_nested_attributes_for :supplier
end

and I am updating the supplier from the account like this : account.update(supplier_attributes: { name: 'new supplier' })

See this test in my dummy app => https://github.com/Tao-Galasse/rails-test-app/blob/main/test/models/account_test.rb

Expected behavior

I expect account.saved_changes to return something like {"supplier_id"=>[1, 2]}

Actual behavior

account.saved_changes returns an empty hash {}

System configuration

Rails version: Tested with Rails 6.1.7.2 & Rails 7.0.4.2

Ruby version: 3.2.0

benngarcia commented 1 year ago

Documentation only mentions using this as the parent class (via has_one and has_many). When using it that way (define the accepts_nested_attributes_for in supplier).

supplier = Supplier.create(name: ‘Supplier 1’, account_attributes: …) dirty works as intended (supplier.account.saved_changes).

However, I would like to be able to use supplier.saved_changes and see what has changes with account IFF I made those modifications via the nested attributes helpers.

My thought is to enable an option on accepts_nested_attribute_for, such as nest_dirty: true, which would show changes made to associations modified via the helper. I.e.

  supplier = Supplier.create(name: ‘supplier 1’, account_attributes: { name: “account 1” })
  supplier.saved_changes
  # => { id: [nil, 1], name: [nil, ‘supplier 1’], account: {  id: [nil, 1], name: [nil, ‘account 1’] } }

Since conceptually the accepts_nested_attributes_for helper is creating a model writer which can be dirtied, I think this would be within their respective domains.

saiqulhaq commented 1 year ago

@Tao-Galasse in your example, you update the name of the supplier only account.update(supplier_attributes: { name: 'new supplier' }) why do you expect the account instance to have a different supplier_id?

I expect account.saved_changes to return something like {"supplier_id"=>[1, 2]}

How come updating the name could change the ID too?

Tao-Galasse commented 1 year ago

@saiqulhaq because i didn't update the existing supplier here, I created a new one.

If I wanted to update the existing one, I should have passed its ID in the supplier_attributes, like this : account.update(supplier_attributes: { id: account.supplier_id, name: 'update my supplier' })

saiqulhaq commented 1 year ago

ok I got it I also found that there is update_only for one on one associations to force always updating the existing record and found this one https://discuss.rubyonrails.org/t/rails-dirty-object-concept-is-not-working-with-nested-att/54505 that raised at 2010 I think the feature to check the saved changes on nested has one and has many won't be implemented because the cost is high

what you can do is enumerate each nested record and check the saved_changes data see https://github.com/saiqulhaq/rails-issue-test-cases/blob/c7885ddda73ea537f41a8a212d14b7a9269e7461/project/48291.rb#L82 as an example

Tao-Galasse commented 1 year ago

In my real life case, I cannot use the update_only: true because my nested record could be shared on other objects where it must not be updated.

But thanks a lot for the example, it could be useful in the future :)