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/reset
  • saved_change_to_attribute? - Whether attribute changed
  • saved_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

  1. Use in callbacks: Most useful in after_save callbacks
  2. Check if changed: Use saved_change_to_attribute? before accessing
  3. Handle nil values: Previous values might be nil for new records
  4. Performance: Be aware of the performance impact in high-traffic scenarios

Limitations

  • Only works after save or reset_*!
  • Doesn't work with update_columns or update_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.

Rails 6.1 adds *_previously_was attribute methods - Abhay Nikam