Почему проверка в модели, на которую ссылается обратный вызов, не приводит к сбою исходной транзакции? - PullRequest
4 голосов
/ 01 марта 2010

У меня есть модель с обратным вызовом after_create. Этот обратный вызов вызывает создание новой записи в другой модели. Однако, если проверка не удалась при создании дочерней записи, исходная транзакция все еще сохраняется.

Это не похоже на правду. Согласно документации Rails, все это заключено в транзакцию. Я что-то не так делаю?

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 терпит неудачу и возвращает ошибку, когда ему передан ноль для cash_account_id, однако мой новый объект ServiceProvision все еще сохраняется.

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 ==)

Это, кажется, противоречит этому из документов

Приходят и Base # save, и Base # destroy завернутый в транзакции, которая обеспечивает что бы вы ни делали в валидации или обратные вызовы произойдут под защищенное покрытие транзакции. Так Вы можете использовать проверки для проверки значения, от которых зависит транзакция или вы можете поднять исключения в обратные вызовы для отката, в том числе after_ * обратные вызовы.

И если я вручную попытаюсь отменить транзакцию в обратном вызове, вот так:

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

затем новый объект ServiceProvision все еще сохраняется.

Ответы [ 4 ]

2 голосов
/ 01 марта 2010

Переместить создание CacheReceipt в фильтр before_validation. Так как у вас есть has_one ассоциация на ServiceProvision, объект CacheReceipt будет иметь правильный :service_provision_id после сохранения. Ваш код будет следующим:

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

Теперь экземпляр сохранения на ServiceProvision вернет false, если при сохранении связанных CacheReceipt.

возникнут ошибки.
1 голос
/ 01 марта 2010

Откат происходит автоматически только с before обратными вызовами:

Вся цепочка обратного вызова заключена в транзакцию. Если какой-либо метод обратного вызова до возвращает точно false или вызывает исключение, цепочка выполнения останавливается и выдается ROLLBACK. После обратных вызовов это можно сделать только путем вызова исключения.

Это имеет смысл, поскольку позволяет AR заполняет модель и сохраняет ее в памяти перед применением транзакции. Поскольку вы сделали after, он также не знает, что откатить. Почему бы не попробовать before_save и посмотреть, что вы получите.

0 голосов
/ 01 марта 2010

Спасибо @KandadaBoggu, который привел меня к решению ...

Оказывается, решение состоит в том, чтобы изменить обратный вызов на before_create, а затем сделать это:

  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

Другими словами, нам все еще нужно вручную проверять ошибки валидации в ассоциации.

0 голосов
/ 01 марта 2010

Необходимо проверить статус выполнения вызова CashReceipt.create в методе receive_payment_for_service_proviion.

  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: Вы можете упростить свою спецификацию after_create следующим образом:

after_create :receive_payment_for_service_provision, :if => :immediate_settlement? 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...