A common problem with background jobs and ActiveRecord is when jobs get enqueued and executed before a needed model is committed to the database.
ActiveRecord models have a nice after_commit
callback that can be used for a particular model.
But let's say you've got some business logic that touches a few different models, and it's not really appropriate to cram that logic inside a single model. So, you write some sort of service/command object that performs the logic inside a transaction block:
For example, something along the lines of:
class SomeServiceObject
def execute
thing = create_thing_in_a_tx
# this notification often fires before the above transaction commits.
notify_user(thing)
end
private
def create_thing_in_a_tx
ActiveRecord::Base.transaction do
a = ModelA.new(foo: 'bar')
b = ModelB.new(a_record: a, biz: 'baz')
#... various other logic that doesn't really belong in a model ...
ThingModel.create!(b_record: b)
end
end
def notify_user(thing)
EnqueueJob.process_asyc(thing.id)
end
end
In this case, as far as I can tell, you don't really have access to the handy after_commit
callback.
I suppose in the above example, you could have ThingModel
enqueue the job inside of its after_commit
callback, but then you're spreading what should be the responsibilities of SomeServiceObject
across different classes, and that feels wrong.
Given all of the above, is there any reasonable way to know when a ActiveRecord::Base.transaction
block commits, without resorting to any particular model's after_commit
callback?
Thank you! :-D
(See also: How to force Rails ActiveRecord to commit a transaction flush)
Transactions in ActiveRecord We're calling the transaction method on the ActiveRecord::Base class and passing it a block. Every database operation that happens inside that block will be sent to the database as a transaction.
Rails transactions are tied to one database connectionAnd as long as the transaction block is running this one database connection is open. So try to do as little as needed inside the transaction block, otherwise you will be blocking a database connection for more time than you should.
You roll back the transaction by raising the ActiveRecord::Rollback error, but the error isn't raised outside, as happens with other errors. Keep this behavior in mind and use it wisely.
Rails has a great, expressive term called pluck that allows you to grab a subset of data from a record. You can use this on ActiveRecord models to return one (or a few) columns. But you can also use the same method on regular old Enumerables to pull out all values that respond to a given key.
It's simpler than you might think. After the ActiveRecord::Base.transaction
block completes, the transaction has been committed.
def create_thing_in_a_tx
begin
ActiveRecord::Base.transaction do
a = ModelA.new(foo: 'bar')
b = ModelB.new(a_record: a, biz: 'baz')
#... various other logic that doesn't really belong in a model ...
ThingModel.create!(b_record: b)
end
# The transaction COMMIT has happened. Do your after commit logic here.
rescue # any exception
# The transaction was aborted with a ROLLBACK.
# Your after commit logic above won't be executed.
end
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With