asibs
asibs

Reputation: 343

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).

Upvotes: 2

Views: 2095

Answers (2)

gmcnaughton
gmcnaughton

Reputation: 2293

Both sides of the association need to be marked with inverse_of. See Rails Guides: Bi-directional Associations.

inverse_of lets Rails know what association keeps the opposite reference from the other model. If set, when you call parent.children.build, the new Child will have its #parent set automatically. That lets it pass the validation check!

Example:

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

Upvotes: 1

Ren
Ren

Reputation: 1374

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

Upvotes: 5

Related Questions