161

Need to check if a block of attributes has changed before update in Rails 3.

street1, street2, city, state, zipcode

I know I could use something like

if @user.street1 != params[:user][:street1]
  then do something....
end

But that piece of code will be REALLY long. Is there a cleaner way?

pcasa
  • 3,710
  • 7
  • 39
  • 67

5 Answers5

296

Check out ActiveModel::Dirty (available on all models by default). The documentation is really good, but it lets you do things such as:

@user.street1_changed? # => true/false
Peter Brown
  • 50,956
  • 18
  • 113
  • 146
59

This is how I solved the problem of checking for changes in multiple attributes.

attrs = ["street1", "street2", "city", "state", "zipcode"]

if (@user.changed & attrs).any?
  then do something....
end

The changed method returns an array of the attributes changed for that object.

Both @user.changed and attrs are arrays so I can get the intersection (see ary & other ary method). The result of the intersection is an array. By calling any? on the array, I get true if there is at least one intersection.

Also very useful, the changed_attributes method returns a hash of the attributes with their original values and the changes returns a hash of the attributes with their original and new values (in an array).

You can check APIDock for which versions supported these methods.

http://apidock.com/rails/ActiveModel/Dirty

onebree
  • 1,853
  • 1
  • 17
  • 44
campeterson
  • 3,591
  • 2
  • 25
  • 26
  • 1
    This is a nice way of doing it. Historically I've done `if attrs.any?{|attr| @user.send("#{attr}_changed?")}` When I want to check if one of several different attributes have changed -- Of course, I only do this with attrs that I've defined myself, because I don't like throwing user params into a `send` method. ;) – nzifnab Sep 11 '13 at 23:50
  • `previously_changed` adaptation, usually in after_commit: `if (@user.previous_changes.keys & attrs).any?` – h0ly Sep 15 '21 at 13:19
18

For rails 5.1+ callbacks

As of Ruby on Rails 5.1, the attribute_changed? and attribute_was ActiveRecord methods will be deprecated

Use saved_change_to_attribute? instead of attribute_changed?

@user.saved_change_to_street1? # => true/false

More examples here

Yarin
  • 173,523
  • 149
  • 402
  • 512
Lucas Andrade
  • 4,315
  • 5
  • 29
  • 50
14

ActiveModel::Dirty didn't work for me because the @model.update_attributes() hid the changes. So this is how I detected changes it in an update method in a controller:

def update
  @model = Model.find(params[:id])
  detect_changes

  if @model.update_attributes(params[:model])
    do_stuff if attr_changed?
  end
end

private

def detect_changes
  @changed = []
  @changed << :attr if @model.attr != params[:model][:attr]
end

def attr_changed?
  @changed.include :attr
end

If you're trying to detect a lot of attribute changes it could get messy though. Probably shouldn't do this in a controller, but meh.

hamstar
  • 1,787
  • 4
  • 16
  • 23
  • 10
    For what it's worth, you can do this with `previous_changes` which is also available by default. – Jason Galuten Oct 29 '15 at 15:17
  • 2
    `#update_attributes` doesn't hide the changes. It saves the record, so the model is updated and there are no changes. You need to change the field in the model and check for `changed?` before you save it. ie. @model.field = 'foo' or `@model.attributes = @model.attributes.merge(params[:model])` – Greg Nov 23 '17 at 06:44
2

Above answers are better but yet for knowledge we have another approch as well, Lets 'catagory' column value changed for an object (@design),

@design.changes.has_key?('catagory')

The .changes will return a hash with key as column's name and values as a array with two values [old_value, new_value] for each columns. For example catagory for above is changed from 'ABC' to 'XYZ' of @design,

@design.changes   # => {} 
@design.catagory = 'XYZ'
@design.changes # => { 'catagory' => ['ABC', 'XYZ'] }

For references change in ROR

shubham mishra
  • 1,183
  • 14
  • 21