【问题标题】:Invalidating parent model save through child before_save callback通过子before_save回调使父模型无效
【发布时间】:2013-06-13 19:25:59
【问题描述】:

我有两个模型,一个父模型和一个子模型(如下所述)。子模型有一个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 调用来解决这个问题。

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

编辑#2

好的,显然让before_save return 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

【问题讨论】:

  • 可以分享一下相关的控制器代码吗?
  • 没有控制器代码。我需要的所有功能都应仅通过模型驱动,并通过控制台保存模型来发挥作用。
  • 是否有特定原因无法将其作为验证运行 (validate :external_logic)?您可以在验证和保存时都使用它。
  • @jokklan 它的validate :something ...
  • @burninggramma 你是对的,对不起。现在已经修复了:)

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


【解决方案1】:

好的,您的问题在于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 =&gt; :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,父对象将被回滚。

如果您不能使用这种格式,您可以尝试使用纯事务(docexample in guides):

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

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

【讨论】:

  • 不,我了解回调顺序以及验证的工作原理。问题是我不能每次模型尝试验证时都调用该函数 - 它只需要运行一次,并且只在保存发生之前。根据我发布的问题,我需要 before_save 回调来使父模型无效,而不是运行其他验证。
  • 在子模型的 before_save 上返回 false 不会使父模型的保存无效 - 父模型仍然保存,因为它的保存调用发生在子模型 before_save 回调甚至运行之前。仅在创建时进行验证不是一种选择 - 当您告诉父模型在保存之前验证其子模型时,调用实际上会运行两次(因此它有一个有效的?由父模型调用,一个作为子模型自己调用)。在事务外部包装事物并不能防止意外实现事务之外的保存。
  • 1.它确实无效,我现在才测试它。你有一个父对象,但它没有提交到数据库中。该对象没有id。它没有坚持下去。 2. the parent model still saves, because its save calls happen before the child before_save callbacks even run => 这不是真的。 3. 可以通过childs验证方法Rails.logger.info,实际上只是在.create上运行一次。
  • 我对那个评论非常怀疑,但我刚刚开发了一个新的 Rails 应用程序,你是对的。我试图弄清楚我的测试和实现中出了什么问题,这样我的before_save 中的false 实际上并没有使父级无效。给我几个小时的时间来弄清楚这一点。
猜你喜欢
  • 2012-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多