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
使用 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