asibs asibs - 2 months ago 13
Ruby Question

Rails ActiveRecord: Saving nested models is rolled back

Using Rails 5:

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


I've created the simplest example I can think of to demonstrate the issue:

parent.rb

class Parent < ApplicationRecord
has_many :children
accepts_nested_attributes_for :children
end


child.rb

class Child < ApplicationRecord
belongs_to :parent
end


Create parent, save, create child, save (works)

Using
rails console
, creating a new parent, then saving, then building a child from the parent, then saving the parent, works fine:

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


Create parent, create child, save (doesn't work)

However, if I try to create a new parent, then build the child without saving the parent, and finally save the parent at the end, the transaction fails and is rolled back:

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


Can anyone explain why, and how to fix?

UPDATE

Creating both parent and child, then saving does work if you pass
validate: false
, so this points to the issue being validation of the child failing, because it requires the parent_id to be set - but presumably the child validation must be running before the parent is saved then, or it wouldn't fail?

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


UPDATE 2

It also works using
save
(without the
validation: false
) if I remove the
belongs_to :parent
line from
child.rb
, since then no validation takes place that
parent_id
is valid before being persisted - however, then you lose ability to get at the parent from the child (via
child.parent
). You can still get to the child from the parent (via
parent.child
).

Ren Ren
Answer

Try it with this:

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

After doing some research I discovered that Rails 5 now requires an associated id to be present in the child by default. Otherwise Rails triggers a validation error.

Check out this article for a great explanation and the relevant pull request

...and the official Rails guide make a very brief mention of it:

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.

So you can turn off this new behavior by adding optional: true after the belongs_to object.

So in your example you would have to create/save Parent first before building the child, or use optional: true