2

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

asibs
  • 243
  • 6
  • 14
  • Out of curiosity, can you try `.save!` and then report what it says? – Dan Rubio Sep 25 '16 at 14:36
  • @DanRubio `ActiveRecord::RecordInvalid: Validation failed: Children parent must exist` - then there's a huge stack trace, but the top line is `from /home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.0.1/lib/active_record/validations.rb:78:in 'raise_validation_error'` – asibs Sep 25 '16 at 14:44
  • I've also just tried save!(validate: false) which works! I will update the question with this info as well... – asibs Sep 25 '16 at 15:02
  • Can you check your Gemfile and post the exact version of rails you are using? – dnsh Sep 25 '16 at 15:28
  • @Dinesh `gem 'rails', '~> 5.0.0', '>= 5.0.0.1'` – asibs Sep 25 '16 at 15:37
  • Oh, and in case it helps, ruby version is `ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]` and `bundle` shows `Using rails 5.0.0.1` – asibs Sep 25 '16 at 15:41

2 Answers2

5

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

Ren
  • 1,379
  • 2
  • 12
  • 24
  • thanks, `optional: true` is working now in the dummy example I posted, I will update my main app in a minute and feedback if that is also working! Seems odd to me that this is now the default behaviour given that it seems pretty standard to create parent/child relationships at the same time from a single form (at least, there seem to be quite a lot of tutorials about it). – asibs Sep 25 '16 at 15:53
  • @ren I was thinking about the same solution. But, In this scenario where we want to save children with parent, this looks like a bug in rails 5. – dnsh Sep 25 '16 at 15:53
  • So this worked with my main app too. I was also validating relationship presence myself with `validates_existence_of` on the 'foreign key' fields, which I obviously needed to get rid of too, since it seems all the validation (both parent and child) is occurring before either entity is actually being persisted to the DB. I expected in such a scenario that rails would validate the parent, persist the parent, validate the child, persist the child. Although I suppose validating all at the start makes sense. But validating 'foreign key' fields before the parent is persisted to DB doesn't seem to... – asibs Sep 25 '16 at 16:02
  • i agree it's weird behavior and looking like a bug that affects nested forms – Ren Sep 25 '16 at 16:22
1

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
gmcnaughton
  • 2,233
  • 1
  • 21
  • 28
  • 1
    I don't think that :inverse_of would work with a polymorphic association. In that case, the better answer is to use :optional => true – jjk Apr 12 '18 at 00:39