On Dec 19, 2014, at 8:18 PM, Jesse Storimer <[email protected]> wrote:
> 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.
>
It seems like a `before_save` that, in case of failure, adds something to
`errors` and returns false would accomplish the same thing here. It would only
run if the validations all otherwise pass, and it would make `save` return
false if it failed.
Something like:
class CarrierService < ActiveRecord::Base
validate :username, :password, presence: true
before_save :validate_credentials
def validate_credentials
@response = external_service.validate(username, password)
unless @response.valid?
errors.add(:base, ‘YOU DONE GOOFED’)
false
end
end
end
> ## 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.
The problem is that explicit transactions are implicitly part of *every* save.
Traversing associations beforehand is a decent approximation, but code that
updates / creates a record in an `after_save` is still going to require time
travel to achieve the intended semantics. In the case of your Product /
ProductImage setup, this would produce a bug:
class Product
has_many :product_images
after_save :create_default_image
def create_default_image
if product_images.empty?
product_images << ProductImage.create(src: ‘some/default/path’)
end
end
end
ProductImage’s before_transaction callback CANNOT run at the right time here,
since it’s fired from a place that MUST already be contained in an open
transaction. Sadly, `gem install tardis` does not help. :)
The before_save suggestion from the beginning of this message has the same
execution-timing problem, but is clearer IMHO because it’s not promising via
naming things that it can’t actually deliver.
—Matt Jones
--
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 [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/rubyonrails-core.
For more options, visit https://groups.google.com/d/optout.
signature.asc
Description: Message signed with OpenPGP using GPGMail
