Why does the validation in the model referenced by the callback cause my original transaction to fail?

I have a model with after_create callback. This callback causes a new record to be created in another model. However, if verification is not performed when creating the child record, the original transaction is still retained.

This does not seem right. According to the Rails docs, all of this is wrapped in a transaction. Am I doing something wrong?

class ServiceProvision < ActiveRecord::Base has_one :cash_receipt after_create :receive_payment_for_service_provision, :if => Proc.new { |sp| sp.immediate_settlement == true } private def receive_payment_for_service_provision cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id ) CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) ) end end class CashReceipt < ActiveRecord::Base belongs_to :service_provision validates_presence_of :cash_account_id end 

CashReceipt exits and returns an error when its passed nil for cash_account_id, however my new ServiceProvision object is still saved.

 it "should fail if a cash account doesn't exist for the currency and institution" do currency = Factory.create( :currency ) institution = Factory.create( :institution ) service_provision = Factory.build( :service_provision, :currency_id => currency.id, :institution_id => institution.id, :immediate_settlement => true ) service_provision.save.should == false service_provision.should have( 1 ).error end 'ServiceProvision service provision creation should raise an error if a cash account doesn't exist for the currency and institution' FAILED expected: false, got: true (using ==) 

This seems to contradict this from the docs.

Both Base # save and Base # destroy come wrapped up in a transaction, which ensures that everything you do in validation or callbacks will have a protected transaction cover. So you can use checks to check the value that the transaction depends on or you can throw exceptions in callbacks to rollback, including after_ * callbacks.

And if I manually try to cancel the transaction in the callback like this:

 cr = CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) ) unless cr.errors.empty? errors.add_to_base("Error while creating CashReciept [#{cr.errors}].") return false end 

then the new ServiceProvision object is saved.

+4
source share
4 answers

Thanks to @KandadaBoggu, which led me to a solution ...

Turns off the solution to change the callback to before_create, and then do the following:

  def receive_payment_for_service_provision cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id ) cr = self.create_cash_receipt( :account_id => account.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) ) unless cr.errors.empty? errors.add_to_base( "Error while creating CashReciept [#{cr.errors}]." ) return false end end 

In other words, we still need to manually check for validation errors in the association.

0
source

Move the CacheReceipt creation to the CacheReceipt filter. Since you have the has_one ServiceProvision association, the has_one object will have the correct one :service_provision_id after saving. Your code will look like this:

 before_validation :receive_payment_for_service_provision, :if => :immediate_settlement? def receive_payment_for_service_provision cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id ) self.cash_receipt.build(:account_id => account.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) ) end 

Now the ServiceProvision save instance returns false if there are errors while saving the associated CacheReceipt .

+2
source

Rollback is performed automatically only with before callbacks:

The entire callback chain ends with a transaction. If any method returns exactly false before the callback or throws an exception, the execution chain stops and ROLLBACK is thrown. Once the callbacks can accomplish this, only by throwing an exception.

This makes sense because it allows AR to refuel the model and store it in memory before applying the transaction. Since you did after , he does not know what to do with the rollback. Why not try before_save and see what you get.

+1
source

You should check the execution status of the CashReceipt.create call in the receive_payment_for_service_proviion method.

  def receive_payment_for_service_provision cash_account = CashAccount.find_by_currency_id_and_institution_id( self.currency_id, self.institution_id ) cr = CashReceipt.create( :account_id => account.id, :service_provision_id => self.id, :amount => self.amount, :currency_id => self.currency.id, :cash_account_id => ( cash_account ? cash_account.id : nil ) ) unless cr.errors.empty? # Make the ServiceProvision instance invalid errors.add_to_base("Error while creating CashReciept [#{cr.errors}].") return false # terminate the callback chain and roll back the TX immediately. end end 

PS: You can simplify the after_create specification as follows:

 after_create :receive_payment_for_service_provision, :if => :immediate_settlement? 
0
source

Source: https://habr.com/ru/post/1302707/


All Articles