Re: [Rails-core] Proposal for before_transaction callback

2014-12-23 Thread Matt Jones

On Dec 19, 2014, at 8:18 PM, Jesse Storimer je...@shopify.com 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 

[Rails-core] Proposal for before_transaction callback

2014-12-22 Thread Jesse Storimer
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.