We have a Rails 6.1 application running an after_commit
callback to look into saved_changes
value to run some background workers. But the issue is saved_changes
returns empty if a model has accepts_nested_attributes_for
. It is not only after_commit
other callbacks like before_save
, and around_save
also return empty saved_changes
value.
For example.
on models/profile.rb
after_commit :run_worker, on: :update
belongs_to :user
accepts_nested_attributes_for :user
In this case saved_changes
is {}
but if I remove the accepts_nested_attributes_for
it works fine and gives me the changed value.
It is a known issue caused by double saves as mentioned on this GH issue. The workaround for 7.1 is adding config on the application.rb
config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = true
This default is for Rails 7.1 and does not work for 6.1. Has someone else run through this issue and has a workaround?
It is not only after_commit other callbacks like
before_save
, andaround_save
also return empty saved_changes value.
saved_change
is only present after a record is saved, so it is expected that it's empty in before_save
callback, and it should be empty in around_save
callback as well until the record is saved there.
Before a record is saved the dirty state is stored in changes
object (not saved_changes
).
To address your problem I would suggest to save changes
in a custom attribute to use later in after_save
/after_commit
callbacks. Something like this:
attr_accessor :my_saved_changes
before_save :save_dirty_state
def save_dirty_state
self.my_saved_changes = changes
end
UPDATED SUGGESTION GIVEN THE COMMENT BELOW:
If I understand correctly those 2 saves happen inside 1 transaction, so after_commit
callback will be called just once. You can probably rely on that and only set my_saved_changes
attribute from my proposal only if it's not set yet (and then reset it in after_commit
callback):
attr_accessor :my_saved_changes
before_save :save_dirty_state
# Have this callback to execute the last:
after_commit :reset_dirty_state
def save_dirty_state
# Note the change of assignment here ('||=' operator instead of '=')
self.my_saved_changes ||= changes
end
def reset_dirty_state
self.my_saved_changes = nil
end