I’d like to propose adding another callback to the ActiveRecord lifecycle that triggers BEFORE the database transaction begins. Think of it as a corollary to after_commit and after_rollback.
## Examples
This is an example resembling something we have to deal with in the Shopify codebase. This is how it could be written using this new callback.
class CarrierService < ActiveRecord::Base
validate :username, :password, presence: true
validate :validate_credentials
before_transaction :initiate_credential_validation
def initiate_credential_validation
logger.debug 'communicating with external carrier...'
@response = external_service.validate(username, password)
end
def validate_credentials
if ! @response.valid?
# add validation error here
end
end
end
Imagine an interaction like this:
carrier = CarrierService.new(username: ‘jesse’, password: ‘password’)
carrier.save
producing a log like this:
communicating with external carrier…
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO “carrier_services” (“username”, “password”) VALUES (?, ?) [[“username”, “jesse”], [“password”, “password”]]
(1.9ms) commit transaction
Note that the external communication happens before the transaction begins.
We are doing this currently in our codebase, but it doesn’t fit nicely into the traditional ActiveRecord callback cycle.
Another obvious place where this would be beneficial is for models that represent assets / uploads that want to fetch the asset on creation but not inside the transaction.
## Motivation
The motivation for this addition is the same as for the after_commit callback. Some models need to communicate with external services in order to do validations / callbacks. To do so inside the DB transaction keeps that transaction open unnecessarily and wreaks havoc on the DB.
## Gotchas
The above example involving a single model being saved with the callback defined is the simple case. It can get more complicated in a scenario like the following:
class Shop < ActiveRecord::Base
end
class ProductImage < ActiveRecord::Base
before_begin :fetch_image
end
shop = Shop.find(1)
shop.images << ProductImage.new(src: ‘…’)
shop.images << ProductImage.new(src: ‘…’)
before opening the transaction to save the shop, we must look for autosave associations
and see if they have before_begin callbacks that need to be triggered
shop.save
We have prototyped a mostly functional supporting this relationship, so it can certainly be accomplished.
This functionality would not play very nicely with the explicit transaction methods.
For instance, when using .transaction this kind of callback could never be triggered reliably.
shop = Shop.find(1)
Shop.transaction do
we’re already in a transaction at this point, so the callback has no hope of being triggered outside of this parent transaction
Product.create(…, shop: shop)
ProductImage.create(…, shop: shop)
end
We would have a little more context when using the #transaction method given that it’s called on an instance that may have this callback defined (or have unsaved associations with this callback defined), but it could be used exactly as in the previous example and circumvent the process.
So the callback would fit in nicely during the regular save
lifecycle, but the explicit transaction methods would be a gotcha when using this feature.