[Feature Proposal] Implement means of tracking which attributes have been set

ActiveRecord::AttributeMethods has a handy #accessed_fields method to track which attributes have been read: https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods.html#method-i-accessed_fields

I would like to add a similar #assigned_fields method to track which attributes have been written.

My use case is that I want to conditionally run certain callbacks only if an attribute has been set, regardless of whether its value changed.

Specifically, I have a Profile model with an approved boolean. Profiles may be of various types (businesses, non-profits, etc.). Some profile types are approved by default, others are not. In a before_save callback, when a profile’s type has changed, its approved boolean must be reset to the new type’s default unless a value for approved has been explicitly provided.

I’ve enabled this in my codebase by adding a couple methods:

class ActiveModel::AttributeSet
  def assigned
    attributes.each_key.select { |name| self[name].send(:assigned?) }
  end
end

module ActiveRecord::AttributeMethods
  # Tracks which attributes have been assigned a value,
  # even if no mutation has occurred.
  def assigned_fields
    @attributes.assigned
  end
end

And my callback conditions look something like this:

  before_save :reset_approval,
    if: proc { |profile|
      profile.profile_type_changed? &&
      !profile.assigned_fields.include?('approved')
    }

I intend to submit a PR implementing this feature, but first wanted to poll the community to get a sense of whether others might find this as useful as I do.

Hey all, wanted to bump this as folks return from RubyConf.

PR here: https://github.com/rails/rails/pull/43575

Just my two cents but seems like a very niche need. Why not just do you tracking in your model. So:

class Profile < ApplicationRecord
  before_save :reset_approval, if: -> { profile_type_changed? && !@assigned_approval }

  def approved= bool
    @assigned_approval = true
    super
  end
end