You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nested callback executions in ActiveRecord (ex: a after_save does a update, which trigger another chain of callbacks) have a weird (IMO wrong) interaction with the saved_change_to_*?, saved_changes, *_before_last_save and friends.
The behavior changed in Rails 5.1. Rails 5.0's behavior was more intuitive.
Steps to reproduce
( I got repro scripts below )
A model with 2 attributes, ex: name and foo
Have a after_save (or any other after_something) that does an update to foo of the model (with a condition, so that you don't get infinite recursion). Ex: update(foo: 1) if foo != 1
Have another after_save (called after the one in (1)) which checks if the other attribute was changed. Ex: $saw_saved_change_to_name = true if saved_change_to_name?
Create an instance setting only the name: Ex: Post.create(name: 'hi')
The second callback will never see saved_change_to_name? as true, because the first callback, triggering anothere save, fully overwrites the tracking.
I added a print of the saved_changes in the test to show what's going on. There is a failing script for main and 5.1, and a passing one for 5.0.
Here is the failing case in main:
# frozen_string_literal: truerequire"bundler/inline"gemfile(true)dosource"https://rubygems.org"git_source(:github){ |repo| "https://github.com/#{repo}.git"}# Activate the gem you are reporting the issue against.gem"rails",github: "rails/rails",branch: "main"gem"sqlite3",'~> 1.4'endrequire"active_record"require"minitest/autorun"require"logger"# This connection will do for database-independent bug reports.ActiveRecord::Base.establish_connection(adapter: "sqlite3",database: ":memory:")ActiveRecord::Base.logger=Logger.new(STDOUT)ActiveRecord::Schema.definedocreate_table:posts,force: truedo |t|
t.text:namet.integer:fooendendclassPost < ActiveRecord::Baseafter_save:set_foo_after_saveafter_save:check_saved_change_to_namedefset_foo_after_saveupdate(foo: 1)iffoo != 1enddefcheck_saved_change_to_nameputs"* Saved changes: #{saved_changes}"
$saw_saved_change_to_name =trueifsaved_change_to_name?endendclassBugTest < Minitest::Testdeftest_association_stuffpost=Post.create!(name: 'hi')assert($saw_saved_change_to_name)endend
* Saved changes: {"foo"=>[nil, 1]}
* Saved changes: {"foo"=>[nil, 1]}
Expected nil to be truthy
Here is the failing case in 5.1:
# frozen_string_literal: truerequire"bundler/inline"gemfile(true)dosource"https://rubygems.org"git_source(:github){ |repo| "https://github.com/#{repo}.git"}# Activate the gem you are reporting the issue against.gem"activerecord","5.1.7"gem"sqlite3",'1.3.13'endrequire"active_record"require"minitest/autorun"require"logger"# This connection will do for database-independent bug reports.ActiveRecord::Base.establish_connection(adapter: "sqlite3",database: ":memory:")ActiveRecord::Base.logger=Logger.new(STDOUT)ActiveRecord::Schema.definedocreate_table:posts,force: truedo |t|
t.text:namet.integer:fooendendclassPost < ActiveRecord::Baseafter_save:set_foo_after_saveafter_save:check_saved_change_to_name_after_savedefset_foo_after_saveupdate(foo: 1)iffoo != 1enddefcheck_saved_change_to_name_after_saveputs"* Saved changes: #{saved_changes}"
$saw_saved_change_to_name =trueifsaved_change_to_name?endendclassBugTest < Minitest::Testdeftest_association_stuffpost=Post.create!(name: 'hi')assert($saw_saved_change_to_name)endend
* Saved changes: {"foo"=>[nil, 1]}
* Saved changes: {"foo"=>[nil, 1]}
Expected nil to be truthy
And here is the passing case in 5.0, which was before Rails switched to saved_change_to_*? and friends:
# frozen_string_literal: truerequire"bundler/inline"gemfile(true)dosource"https://rubygems.org"git_source(:github){ |repo| "https://github.com/#{repo}.git"}# Activate the gem you are reporting the issue against.gem"activerecord","5.0.7.2"gem"sqlite3",'1.3.13'endrequire"active_record"require"minitest/autorun"require"logger"# This connection will do for database-independent bug reports.ActiveRecord::Base.establish_connection(adapter: "sqlite3",database: ":memory:")ActiveRecord::Base.logger=Logger.new(STDOUT)ActiveRecord::Schema.definedocreate_table:posts,force: truedo |t|
t.text:namet.integer:fooendendclassPost < ActiveRecord::Baseafter_create:set_foo_after_saveafter_save:check_saved_change_to_name_after_savedefset_foo_after_saveupdate(foo: 1)iffoo != 1enddefcheck_saved_change_to_name_after_saveputs"* Saved changes: #{changes}"
$saw_saved_change_to_name =trueifname_changed?endendclassBugTest < Minitest::Testdeftest_association_stuffpost=Post.create!(name: 'hi')assert($saw_saved_change_to_name)endend
* Saved changes: {"id"=>[nil, 1], "name"=>[nil, "hi"], "foo"=>[nil, 1]}
* Saved changes: {}
Passes the test
Expected behavior
I expect a after_* callback that reacts to saved_change_to_*? to be called at least once with said change of true when the attribute gets changed.
Actual behavior
The callback never gets called with saved_change_to_name? being true because the nested change
In my opinion, the priority should be on handling the 1st expected behavior
This means that if I make code with such a callback, everything could work, and someone doing a nested update in a different callback could completely break the first callback.
System configuration
Rails version: 5.1, main
Ruby version: 2.4 and 3.1
The text was updated successfully, but these errors were encountered:
Avoid updating or saving attributes in callbacks. For example, don't call update(attribute: "value")
within a callback.
This can alter the state of the model and may result in unexpected side effects during commit.
Instead, you can safely assign values directly (for example, self.attribute = "value") in before_create / before_update or earlier callbacks.
Nested callback executions in ActiveRecord (ex: a after_save does a update, which trigger another chain of callbacks) have a weird (IMO wrong) interaction with the
saved_change_to_*?
,saved_changes
,*_before_last_save
and friends.The behavior changed in Rails 5.1. Rails 5.0's behavior was more intuitive.
Steps to reproduce
( I got repro scripts below )
A model with 2 attributes, ex:
name
andfoo
Have a after_save (or any other after_something) that does an update to
foo
of the model (with a condition, so that you don't get infinite recursion). Ex:update(foo: 1) if foo != 1
Have another after_save (called after the one in (1)) which checks if the other attribute was changed. Ex:
$saw_saved_change_to_name = true if saved_change_to_name?
Create an instance setting only the name: Ex:
Post.create(name: 'hi')
The second callback will never see
saved_change_to_name?
as true, because the first callback, triggering anothere save, fully overwrites the tracking.I added a print of the saved_changes in the test to show what's going on. There is a failing script for main and 5.1, and a passing one for 5.0.
Here is the failing case in main:
Here is the failing case in 5.1:
And here is the passing case in 5.0, which was before Rails switched to
saved_change_to_*?
and friends:Expected behavior
I expect a
after_*
callback that reacts tosaved_change_to_*?
to be called at least once with said change oftrue
when the attribute gets changed.Actual behavior
The callback never gets called with
saved_change_to_name?
beingtrue
because the nested changeIn my opinion, the priority should be on handling the 1st expected behavior
This means that if I make code with such a callback, everything could work, and someone doing a nested update in a different callback could completely break the first callback.
System configuration
Rails version: 5.1, main
Ruby version: 2.4 and 3.1
The text was updated successfully, but these errors were encountered: