Rails 6.1 adds *_previously_was attribute methods
Rails 6.1 introduced *_previously_was attribute methods that allow you to access the previous value of an attribute after a model has been saved or reset. This extends Rails' dirty tracking capabilities.
What is *_previously_was?
The *_previously_was methods provide access to the previous value of an attribute after changes have been persisted or reset. This is different from *_was, which only works before saving.
Before Rails 6.1
Previously, you could only access previous values before saving:
user = User.find(1)
user.name = 'New Name'
# Works before save
user.name_was # => "Old Name"
user.save
# Doesn't work after save
user.name_was # => nil
Rails 6.1 Solution
Rails 6.1 adds *_previously_was methods that work after saving:
user = User.find(1)
user.name = 'New Name'
user.save
# Works after save
user.name_previously_was # => "Old Name"
Basic Usage
After Save
Access previous values after saving:
user = User.find(1)
old_name = user.name # => "John"
user.name = 'Jane'
user.save
user.name # => "Jane"
user.name_previously_was # => "John"
After Reset
Access previous values after resetting changes:
user = User.find(1)
user.name = 'New Name'
user.name_previously_was # => nil (not saved yet)
user.reset_name!
user.name # => "John" (original value)
user.name_previously_was # => "New Name" (previous change)
Real-World Examples
Logging Changes
Log what changed after saving:
class User < ApplicationRecord
after_save :log_name_change, if: :saved_change_to_name?
private
def log_name_change
ActivityLog.create(
user: self,
action: 'name_changed',
old_value: name_previously_was,
new_value: name
)
end
end
Sending Notifications
Send notifications when important fields change:
class User < ApplicationRecord
after_save :notify_email_change, if: :saved_change_to_email?
private
def notify_email_change
EmailChangeNotificationMailer.notify(
user: self,
old_email: email_previously_was,
new_email: email
).deliver_later
end
end
Audit Trail
Create audit records:
class Post < ApplicationRecord
after_save :create_audit_record, if: :saved_changes?
private
def create_audit_record
AuditRecord.create(
record_type: 'Post',
record_id: id,
changes: saved_changes.transform_values { |v| v[0] }, # old values
previous_values: saved_changes.transform_values { |v| v[1] } # new values
)
end
end
Comparison with Other Methods
*_was (Before Save)
user.name = 'New Name'
user.name_was # => "Old Name" (works before save)
user.save
user.name_was # => nil (doesn't work after save)
*_previously_was (After Save)
user.name = 'New Name'
user.name_previously_was # => nil (not saved yet)
user.save
user.name_previously_was # => "Old Name" (works after save)
saved_change_to_*?
user.name = 'New Name'
user.save
user.saved_change_to_name? # => true
user.saved_change_to_name # => ["Old Name", "New Name"]
Available Methods
For each attribute, Rails provides:
attribute_previously_was- Previous value after save/resetsaved_change_to_attribute?- Whether attribute changedsaved_change_to_attribute- Array of [old_value, new_value]
Multiple Attributes
Check multiple attributes:
user = User.find(1)
user.name = 'New Name'
user.email = 'new@example.com'
user.save
user.name_previously_was # => "Old Name"
user.email_previously_was # => "old@example.com"
Use Cases
Change Detection
Detect what changed:
class User < ApplicationRecord
after_save :handle_name_change, if: :saved_change_to_name?
private
def handle_name_change
if name_previously_was.present?
# Name was changed
update_search_index
else
# Name was set for the first time
send_welcome_email
end
end
end
Conditional Logic
Perform actions based on previous values:
class Order < ApplicationRecord
after_save :handle_status_change, if: :saved_change_to_status?
private
def handle_status_change
case status_previously_was
when 'pending'
notify_customer_approved
when 'approved'
notify_customer_shipped
end
end
end
Data Migration
Track data migrations:
class Migration < ApplicationRecord
after_save :log_migration, if: :saved_change_to_completed?
private
def log_migration
if completed_previously_was == false && completed == true
MigrationLog.create(
migration: self,
completed_at: Time.current
)
end
end
end
Best Practices
- Use in callbacks: Most useful in
after_savecallbacks - Check if changed: Use
saved_change_to_attribute?before accessing - Handle nil values: Previous values might be nil for new records
- Performance: Be aware of the performance impact in high-traffic scenarios
Limitations
- Only works after
saveorreset_*! - Doesn't work with
update_columnsorupdate_all - Previous values are cleared after the next save
Conclusion
Rails 6.1's *_previously_was methods extend dirty tracking to work after saves and resets. This feature is particularly useful for logging changes, sending notifications, and creating audit trails. It provides a clean way to access previous attribute values in callbacks and other post-save operations.