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 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 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.
signature.asc
Description: Message signed with OpenPGP using GPGMail