我有两个模型,父母和孩子(如下所述)。子模型有一个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
答案 0 :(得分:5)
好的,问题在于ActiveRecord::Callbacks订单。
正如您在链接页面上看到的那样,首次验证处理完毕,如果验证成功,则运行before_save回调。 before_save
是一个可以假设每次验证都通过的地方,因此您可以根据其他属性操作位数据或填充自定义属性。这样的事情。
所以你能做的只是对Child
模型说:
validate :external_logic
,只需删除before_save :external_logic
回调即可。
它与您想要做的相同。当创建Parent
实例时,如果Child
对象无法验证,则会发生错误,这将在您的:external_logic验证方法中发生。这是custom validation method technique。
仍然可以使用: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,则将回滚父对象。
或强>
如果您无法使用此格式,您可以尝试使用纯交易(doc,example in guides):
Parent.transaction do
p = Parent.create
raise Exception if true # any condition
end
如果在块内引发异常,则会回滚您在此事务中执行的任何操作。