Upgrading to Rails 5.1 on the ActiveRecord side
Our method to upgrade Rails is somehow similar to upgrading any other ruby gem: changing the version in the Gemfile
, call bundler upgrade <gem>
and run out test suite to check what’s broken. While this is rather painless for a small gem, when it comes to Rails, that can be much more complicated.
Moving from 5.0 to 5.1, our test suite showed up a few deprecation messages:
DEPRECATION WARNING: The behavior of
changed?
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, usesaved_changes?
instead.DEPRECATION WARNING: The behavior of
changed_attributes
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, usesaved_changes.transform_values(&:first)
instead.DEPRECATION WARNING: The behavior of
attribute_changed?
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, usesaved_change_to_attribute?
instead.DEPRECATION WARNING: The behavior of
attribute_was
inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method aftersave
returned (e.g. the opposite of what it returns now). To maintain the current behavior, useattribute_before_last_save
instead.
These messages are relatively common while upgrading Rails, the framework announce the changes to come in the next version so that developers can take the time to adapt their code. Being just deprecation warnings, we did not quite notice them during Rails beta/rc phase. But as we more heavily test the behavior of out application, some part where not iso-functional to the previous version.
Digging a little, we were surprised not to find a note in the official Changelog, nor in the large amount of blog post which rise around the new releases of Rails. And the explanation were right in the Rails repository, precisely in this commit:
Deprecate the behavior of AR::Dirty inside of after_(create|update|save) callbacks
We pretty frequently get bug reports that “dirty is broken inside of after callbacks”. Intuitively they are correct. You’d expect
Model.after_save { puts changed? }; model.save
to do the same thing asmodel.save; puts model.changed?
, but it does not.However, changing this goes much farther than just making the behavior more intuitive. There are a ton of places inside of AR that can be drastically simplified with this change. Specifically, autosave associations, timestamps, touch, counter cache, and just about anything else in AR that works with callbacks have code to try to avoid “double save” bugs which we will be able to flat out remove with this change.
We introduce two new sets of methods, both with names that are meant to be more explicit than dirty. The first set maintains the old behavior, and their names are meant to center that they are about changes that occurred during the save that just happened. They are equivalent to
previous_changes
when called outside of after callbacks, or once the deprecation cycle moves.The second set is the new behavior. Their names imply that they are talking about changes from the database representation. The fact that this is what we really care about became clear when looking at
BelongsTo.touch_record
when tests were failing. I’m unsure that this set of methods should be in the public API. Outside of after callbacks, they are equivalent to the existing methods on dirty.Dirty itself is not deprecated, nor are the methods inside of it. They will only emit the warning when called inside of after callbacks. The scope of this breakage is pretty large, but the migration path is simple. Given how much this can improve our codebase, and considering that it makes our API more intuitive, I think it’s worth doing.
Although marked as DEPRECATION WARNING
, so for the next release of Rails, these change applies to the very current version we try to upgrade to. The commit is from June, 9 2017, may be the change would have been included in the 5.0 release which came on June, 30, and has been postponed on the last minute, so the deprecation message only appears now we are on the 5.1 branch? Nonetheless, change is around the corner and we have to deal with it.
Model callbacks behavior
After Rails 4.x release, some changes were made on callbacks to split update on the model before and after writing data to the database. We had to be careful to use the correct helpers: <attribute>_changed?
for after_save
callbacks, and previous_changes[:<attributes>]
for after_commit
callbacks:
# Rails 5.0
after_save :update_customer_details!, if: :status_changed?
after_commit :create_new_email_address!, if: -> { previous_changes[:email] }
With Rails 5.1, the code above becomes more coherent and even simpler to understand, as we can use the new helper saved_changed_to_<attribute>
in both callbacks:
# Rails 5.1
after_save :update_customer_details!, if: :saved_changes_to_status?
after_commit :create_new_email_address!, if: :saved_changes_to_email?
Using the <attribute>_changed?
helper along saved_change_to_<attribute>
is now more comprehensible:
user = User.last
user.name # => "Bob"
user.name_changed? # => false
user.saved_change_to_name? # => false
user.name = "Clément" # => "Clément"
user.name_changed? # => true
user.saved_change_to_name? # => false
user.save # => true
user.name # => "Clément"
user.name_changed? # => false
user.saved_change_to_name? # => true
But let’s see what’s going on inside callbacks when they are triggered by placing breakpoints in our User
model:
class User < ApplicationRecord
after_save { binding.pry }
after_commit { binding.pry }
end
user = User.last
user.name # => "Clément"
user.changed? # => false
user.saved_changes? # => false
user.name = "Bob" # => "Bob"
user.changed? # => true
user.saved_changes? # => false
user.save!
From: /Users/bob/Work/test/app/models/user.rb @ line 2 :
1: class User < ApplicationRecord
=> 2: after_save { binding.pry }
3: after_commit { binding.pry }
4: end
[1] > changed?
DEPRECATION WARNING: The behavior of `changed?` inside of after callbacks will
be changing in the next version of Rails. The new return value will reflect the
behavior of calling the method after `save` returned (e.g. the opposite of what
it returns now). To maintain the current behavior, use `saved_changes?`
instead.
=> true
[2] > name_changed?
DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after
callbacks will be changing in the next version of Rails. The new return value
will reflect the behavior of calling the method after `save` returned (e.g. the
opposite of what it returns now). To maintain the current behavior, use
`saved_change_to_attribute?` instead.
=> true
[3] > previous_changes
DEPRECATION WARNING: The behavior of `previous_changes` inside of after
callbacks is deprecated without replacement. In the next release of Rails, this
method inside of `after_save` will return the changes that were just saved.
=> {}
[4] > saved_change_to_name?
=> true
[5] > name_before_last_save
=> "Clément"
[6] > name
=> "Bob"
[6] > exit
From: /Users/bob/Work/test/app/models/user.rb @ line 3 :
1: class User < ApplicationRecord
2: after_save { binding.pry }
=> 3: after_commit { binding.pry }
4: end
[1] > saved_change_to_name?
=> true
[2] > name_before_last_save
=> "Clément"
[3] > name
=> "Bob"
[4] > previous_changes[:name]
=> ["Clément", "Bob"]
[5] > exit
Here we can see saved_change_to_name?
behaves the same in the 2 callbacks after_save
and after_commit
. It’s very pleasing not to have to ask ourself which helper to use.
Also note previous_changes
still exists, and only works in a after_commit
callback (with a weird deprecation messages). We would only use this method to fetch a hash of updated attributes with both their previous and current values.
To summarize
From Rails 5.1, new helper methods are introduced to clarify and simplify usage of dirty attributes inside models callbacks, and replace the old ambiguous ones:
Rails 5.0 Rails 5.1+ <attribute>_changed?
→ saved_change_to_<attribute>?
<attribute>_was
→ <attribute>_before_last_save
changed?
→ saved_changes?
changed_attributes
→ saved_changes.transform_values(&:first)
Be sure to use them right when upgrading to Rails 5.1. Check your logfiles, as even if it seem to only be a deprecation, new behavior applies.