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.
--
You received this message because you are subscribed to the Google Groups Ruby
on Rails: Core group.
To unsubscribe from this group and stop receiving emails from it, send an email
to rubyonrails-core+unsubscr...@googlegroups.com.
To post to this group, send email to rubyonrails-core@googlegroups.com.
Visit this group at http://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.