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
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
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
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.
Top comments (1)
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.