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.

Reply via email to