Rails ActiveRecord:回滚保存嵌套模型

Rails ActiveRecord: Saving nested models is rolled back

使用 Rails 5:

gem 'rails', '~> 5.0.0', '>= 5.0.0.1'

我已经创建了我能想到的最简单的示例来演示该问题:

parent.rb

class Parent < ApplicationRecord
  has_many :children
  accepts_nested_attributes_for :children
end

child.rb

class Child < ApplicationRecord
  belongs_to :parent
end

创建 parent,保存,创建 child,保存(有效)

使用 rails console,创建一个新的 parent,然后保存,然后从 parent 构建一个 child,然后保存 parent,有效很好:

irb(main):004:0> parent = Parent.new
=> #<Parent id: nil, created_at: nil, updated_at: nil>
irb(main):005:0> parent.save
   (0.5ms)  BEGIN
  SQL (0.4ms)  INSERT INTO `parents` (`created_at`, `updated_at`) VALUES ('2016-09-25 13:05:44', '2016-09-25 13:05:44')
   (3.2ms)  COMMIT
=> true
irb(main):006:0> parent.children.build
=> #<Child id: nil, parent_id: 1, created_at: nil, updated_at: nil>
irb(main):007:0> parent.save
   (0.5ms)  BEGIN
  Parent Load (0.5ms)  SELECT  `parents`.* FROM `parents` WHERE `parents`.`id` = 1 LIMIT 1
  SQL (0.7ms)  INSERT INTO `children` (`parent_id`, `created_at`, `updated_at`) VALUES (1, '2016-09-25 13:05:52', '2016-09-25 13:05:52')
   (1.3ms)  COMMIT
=> true

创建 parent、创建 child、保存(不起作用)

但是,如果我尝试创建一个新的 parent,然后在不保存 的情况下构建 child ,最后保存最后的parent,事务失败回滚:

irb(main):008:0> parent = Parent.new
=> #<Parent id: nil, created_at: nil, updated_at: nil>
irb(main):009:0> parent.children.build
=> #<Child id: nil, parent_id: nil, created_at: nil, updated_at: nil>
irb(main):010:0> parent.save
   (0.5ms)  BEGIN
   (0.4ms)  ROLLBACK
=> false

谁能解释原因,以及如何解决?

更新

同时创建 parent 和 child,然后如果您通过 validate: false,保存确实有效,所以这表明问题是 child 的验证失败,因为它需要设置 parent_id - 但大概 child 验证必须 运行 之前 parent 被保存,或者它不会失败?

irb(main):001:0> parent = Parent.new
=> #<Parent id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> parent.children.build
=> #<Child id: nil, parent_id: nil, created_at: nil, updated_at: nil>
irb(main):003:0> parent.save(validate: false)
   (0.7ms)  BEGIN
  SQL (0.9ms)  INSERT INTO `parents` (`created_at`, `updated_at`) VALUES ('2016-09-25 15:02:20', '2016-09-25 15:02:20')
  SQL (0.8ms)  INSERT INTO `children` (`parent_id`, `created_at`, `updated_at`) VALUES (3, '2016-09-25 15:02:20', '2016-09-25 15:02:20')
   (1.6ms)  COMMIT
=> true

更新 2

如果我从 child.rb 中删除 belongs_to :parent 行,它也可以使用 save(没有 validation: false),从那时起就不会验证 [=23] =] 在被持久化之前是有效的——但是,然后你就失去了从 child 获取 parent 的能力(通过 child.parent)。您仍然可以从 parent(通过 parent.child)到达 child。

试试这个:

class Child < ApplicationRecord
  belongs_to :parent, optional: true
end

经过一些研究后,我发现 Rails 5 现在需要一个关联的 ID 才能在子 默认情况下出现 。否则 Rails 触发验证错误。

看看这个 article for a great explanation and the relevant pull request

...官方Rails guide 非常简单地提到了它:

4.1.2.11 :optional

If you set the :optional option to true, then the presence of the associated object won't be validated. By default, this option is set to false.

因此,您可以通过在 belongs_to 对象后添加 optional: true 来关闭此新行为。

因此,在您的示例中,您必须先 create/save 父级,然后再构建子级,或者使用 optional: true

关联双方都需要用inverse_of标记。参见 Rails Guides: Bi-directional Associations

inverse_of 让 Rails 知道是什么关联保留了与其他模型相反的引用。如果设置,当您调用 parent.children.build 时,新的子项将自动设置其 #parent。这让它通过了验证检查!

示例:

class Parent < ApplicationRecord
  has_many :children, inverse_of: :parent
  accepts_nested_attributes_for :children
end

class Child < ApplicationRecord
  belongs_to :parent, inverse_of: :children
end

> parent = Parent.new
 => #<Parent id: nil, created_at: nil, updated_at: nil>
> parent.children.build
 => #<Child id: nil, parent_id: nil, created_at: nil, updated_at: nil>
> parent.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "parents" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", 2016-09-26 00:46:42 UTC], ["updated_at", 2016-09-26 00:46:42 UTC]]
  SQL (0.1ms)  INSERT INTO "children" ("parent_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["parent_id", 2], ["created_at", 2016-09-26 00:46:42 UTC], ["updated_at", 2016-09-26 00:46:42 UTC]]
   (1.8ms)  commit transaction
 => true