DEV Community

Thibault
Thibault

Posted on

Tracking Rails changed attributes

On today's "What did i want to achieve/how i achieved it", I had to fix this weird bug where one form field was triggering changes in the DB regardless of any user interaction.

Take your typical rails form and add a map component(we deal a lot with location data at Georepublic).

The bug was that, when a user edits one record's attribute and save, this would also update the location attributes(Latitude & Longitude) automatically by 1/1000000000 which isn't meaningful TBH but the issue was emailing subscribed users on such minimal changes!

PS: The purpose of this post is not fixing that value-change bug, but detecting and ignoring those small changes

Meet ActiveModel::Dirty

Rails comes with ActiveModel::Dirty module that, as it's described,

Provides a way to track changes in your object in the same way as Active Record does.

This allows you to perform various operations whenever your attributes changes.

Going back to my bug, i had to make sure the location changes were "valid enough" to trigger our notification services.

Step 1: Add callback

Since our notification service is called on every update, it makes sense to check for these changes "before updating"

before_update: ignore_small_changes
Enter fullscreen mode Exit fullscreen mode

Step 2: Track changes

As mentioned earlier, this was a location attribute saved as a geometry point in the db (cfr Rgeo) which basically mean that it's a collection of points a.k.a [140.1250590699026,35.6097256061325]

Using Rails ActiveModel::Dirty you can detect changes on any attribute using the following ways:

class User < ApplicationRecord
end

user = User.first
user.update(address: "new address")
user.address_changed? #returns true if user address has changed

user.address_change # returns an array of [oldvalue, newvalue] or [nil] if there's no change
Enter fullscreen mode Exit fullscreen mode

Step 3: Ignoring small changes

Since we are dealing with a location point i.e [lat,lng] calling address_change on this would return a collection of this kind [[lat, lng],[lat, lng]]

Thus, comparing changes(and ignoring small ones) in these two collections become a lot easier with Ruby#Zip

def ignore_small_changes
  unless address_change[0].nil?
    old_value = address_change[0].coordinates
    new_value = address_change[1].coordinates
#Ignore changes if the difference is minimal
    self.address = address_change[0] if new_value.zip(old_value).map { |a, b| (a-b).abs }.map {|x| x < 0.00000001}.all?
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

Although this was used to fix a bug in my case, you can always leverage these methods by using it as callbacks to performing different actions only when certain attributes have changed.

Discussion (1)

Collapse
arvindvyas profile image
Arvind

class User < ApplicationRecord
end

user = User.first
user.update(address: "new address")
user.address_changed? #returns true if user address has changed

user.address_change

I don't think so this will work, this only work before update not after an update.