通过子before_save回调使父模型保存无效

时间:2013-06-13 19:25:59

标签: ruby-on-rails ruby-on-rails-3

我有两个模型,父母和孩子(如下所述)。子模型有一个before_save回调来处理一些外部逻辑,如果遇到任何错误,回调会使保存的模型无效。

class Parent < ActiveRecord::Base
  has_one :child
  accepts_nested_attributes_for :child

  validates :child, :presence => true
  validates_associated :child
end

class Child < ActiveRecord::Base
  belongs_to :parent

  before_save :external_logic
  validates :parent, :presence => true

  def external_logic
    begin
      # Some logic
    rescue
      #Invalidate child model
      errors.add(:base, "external logic failed")
      return false
    end
  end
end

我遇到的问题是Child模型实例是通过Parent模型的嵌套属性创建的。当外部逻辑失败时,我希望不保存子模型和父模型,而是保存父模型。我怎样才能做到这一点?

请注意,我知道验证回调,但在这种情况下它们并不合适。子模型回调必须是before_save。

编辑#1

我已经了解交易,并且不认为有人告诉我“嘿,将其包装在外部交易中”作为有效的回复。这个问题明确是关于如何通过before_save调用来解决这个问题。

为什么我不能在create上使用验证 - 正如评论中所提到的,逻辑的外部位需要保证仅在数据库保存之前运行。无论是否更改数据库记录,验证调用都可能发生多次,因此这是一个不适合放置此逻辑的地方。

编辑#2

好的,显然before_save返回false会阻止父项被保存。我已通过控制台验证并实际检查数据库。但是,我的rspec测试告诉了我,这只是奇怪的。特别是,这是失败的:

describe "parent attributes hash" do
  it "creates new record" do
    parent = Parent.create(:name => "name", :child_attributes => {:name => "childname"})
    customer.persisted?.should be_false
  end
end

这可能是一个很奇怪的rspec / factory_girl吗?

编辑#3

测试错误是因为我在Rspec中使用事务夹具。这导致测试错误地告诉我,当数据库确实不存在时,数据库中存在对象。

config.use_transactional_fixtures = true

1 个答案:

答案 0 :(得分:5)

好的,问题在于ActiveRecord::Callbacks订单。

正如您在链接页面上看到的那样,首次验证处理完毕,如果验证成功,则运行before_save回调before_save是一个可以假设每次验证都通过的地方,因此您可以根据其他属性操作位数据或填充自定义属性。这样的事情。

所以你能做的只是对Child模型说:
validate :external_logic,只需删除before_save :external_logic回调即可。

它与您想要做的相同。当创建Parent实例时,如果Child对象无法验证,则会发生错误,这将在您的:external_logic验证方法中发生。这是custom validation method technique

OP更新后:

仍然可以使用:validate方法。您可以将其设置为仅在create上运行:
validate :external_logic, :on => :create

如果您遇到问题需要在update上运行,那么这是默认行为。验证仅在.create和.update上运行。

或者如果你想坚持使用before_save:

The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.

我看到你做了return false所以它应该按预期工作。你如何使用Parent.create!方法?有什么争论?

确保您使用它(假设.name是父和子的属性):

Parent.create!{
  :name => 'MyParent'
  # other attributes for Parent
  :child_attributes => { 
    :name => 'MyChild'
    # other attributes for Child
  } 
}

这样它将在同一个事务中创建Parent和Child对象,因此如果您的before_save方法返回false,则将回滚父对象。

如果您无法使用此格式,您可以尝试使用纯交易(docexample in guides):

Parent.transaction do
  p = Parent.create
  raise Exception if true # any condition
end

如果在块内引发异常,则会回滚您在此事务中执行的任何操作。