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

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

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

编辑#1

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

为什么我不能在创建时使用验证 - 如评论中所述,需要保证外部逻辑位仅在数据库保存之前运行。无论是否更改数据库记录,验证调用都可能发生多次,因此不适合放置此逻辑。

编辑#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
4

1 回答 1

6

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

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

所以你可以做的只是对Child模型说:
validate :external_logic然后删除before_save :external_logic回调。

它等同于您想要做的事情。创建Parent实例时,如果Child对象无法验证,它只会出错,这将在您的 :external_logic 验证方法中发生。这是一种自定义验证方法技术

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 和 Child 的属性):

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

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

或者

如果您不能使用这种格式,您可以尝试使用纯事务(文档指南中的示例):

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

如果在块内引发异常,您在此事务中所做的任何事情都将被回滚。

于 2013-06-16T22:17:47.030 回答