Active Record dirty tracking in after_commit hooks
Rails provides a convenient way to track changes in Active Record models with the
ActiveRecord::AttributeMethods::Dirty module. This allows us
to check what changes are going to be persisted to the database or the latest changes that were persisted.
These methods can be useful in Active Record hooks so that you can run certain hooks only when certain attributes change. For example:
class Book < ApplicationRecord after_commit do do_something if saved_change_to_title? end end
Though there is one little quirk when using this inside
after_commit hooks. When an explicit transaction is started, and the model is saved
multiple times within the transaction, the
after_commit hook will only be called once.
That makes sense because these updates are committed at the same time at the end of the transaction. But what happens to Active Record’s dirty tracking? It would only reflect the changes in the latest save because dirty tracking is reset after every save even if they’re not committed yet.
So for example we have something like this:
book = Book.find(1) Book.transaction do book.update!(title: 'A new title') book.update!(updated_at: Time.current) end
after_commit hook is called,
saved_change_to_title? will return
saved_changes will only contain the changes to
One way to work around this is to do our own dirty tracking that is cleared after commit:
class Book < ApplicationRecord after_save do @title_changed = true if saved_change_to_title? end after_commit do do_something if @title_changed @title_changed = nil end end
This allows us to run
after_commit hooks conditionally based on attribute changes.