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
When the after_commit
hook is called, saved_change_to_title?
will return false
because saved_changes
will only contain the changes to updated_at
.
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.