Rails Activerecord: Saving Nested Models Is Rolled Back

Rails ActiveRecord: Saving nested models is rolled back

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

Rails 5 - save rolls back because nested models parent model is not being saved before child model

Alright I rephrased the question on another question and I finally found the answer to this. So I am pasting my answer from there, in case someone searches for the issue in the same fashion that I was asking the question here.

Ok, I am answering my own question because I know many people are struggling with this and I actually have the answer and not a vague response to the documentation.

First we will just be using a one to one relationship for this example. When you create your relationships you need to make sure that the parent model has the following

  1. inverse_of:
  2. autosave: true
  3. accepts_nested_attributes_for :model, allow_destroy:true

Here is the Users model then I will explain,

class User < ApplicationRecord
has_one :profile, inverse_of: :user, autosave: true
accepts_nested_attributes_for :profile, allow_destroy: true
end

in Rails 5 you need inverse_of: because this tells Rails that there is a relationship through foreign key and that it needs to be set on the nested model when saving your form data. Now if you were to leave autosave: true off from the relationship line you are left with the user_id not saving to the profiles table and just the other columns, unless you have validations off and then it won't error out it will just save it without the user_id. What is going on here is autosave: true is making sure that the user record is saved first so that it has the user_id to store in the nested attributes for the profile model.
That is it in a nutshell why the user_id was not traversing to the child and it was rolling back rather than committing.
Also one last gotcha is there are some posts out there telling you in your controller for the edit route you should add @user.build_profile like I have in my post. DO NOT DO IT THEY ARE DEAD WRONG, after assessing the console output it results in

Started GET "/users/1/edit" for 192.168.0.31 at 2017-03-12 22:38:17 -0400
Cannot render console from 192.168.0.31! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by UsersController#edit as HTML
Parameters: {"id"=>"1"}
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
Profile Load (0.5ms) SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`user_id` = 1 LIMIT 1
(0.1ms) BEGIN
SQL (0.5ms) UPDATE `profiles` SET `user_id` = NULL, `updated_at` = '2017-03-13 02:38:17' WHERE `profiles`.`id` = 1
(59.5ms) COMMIT
Rendering users/edit.html.erb within layouts/application
Rendered users/_form.html.erb (44.8ms)
Rendered users/edit.html.erb within layouts/application (50.2ms)
Completed 200 OK in 174ms (Views: 98.6ms | ActiveRecord: 61.1ms)

If you look it is rebuilding the profile from scratch and resetting the user_id to null for the record that matches the current user you are editing. So be very careful of this as I have seen tons of posts making this suggestion and it cost me DAYS of research to find a solution!

Rollback entire transaction within nested transaction

You can make sure the transactions are not joinable

User.transaction(joinable:false) do 
User.create(username: 'Kotori')
User.transaction(requires_new: true, joinable: false) do
User.create(username: 'Nemu') and raise ActiveRecord::Rollback
end
end

This will result in something akin to:

SQL (12.3ms)  SAVE TRANSACTION active_record_1
SQL (11.7ms) SAVE TRANSACTION active_record_2
SQL (11.1ms) ROLLBACK TRANSACTION active_record_2
SQL (13.6ms) SAVE TRANSACTION active_record_2
SQL (10.7ms) SAVE TRANSACTION active_record_3
SQL (11.2ms) ROLLBACK TRANSACTION active_record_3
SQL (11.7ms) ROLLBACK TRANSACTION active_record_2

Where as your current example results in

SQL (12.3ms)  SAVE TRANSACTION active_record_1
SQL (13.9ms) SAVE TRANSACTION active_record_2
SQL (28.8ms) ROLLBACK TRANSACTION active_record_2

While requires_new: true creates a "new" transaction (generally via a save point) the rollback only applies to that transaction. When that transaction rolls back it simply discards the transaction and utilizes the save point.

By using requires_new: true, joinable: false rails will create save points for these new transactions to emulate the concept of a true nested transaction and when the roll back is called it will rollback all the transactions.

You can think of it this way:

  • requires_new: true keeps this transaction from joining its parent
  • joinable: false means the parent transaction cannot be joined by its children

When using both you can ensure that any transaction is never discarded and that ROLLBACK anywhere will result in ROLLBACK everywhere.

How to save a nested resource in ActiveRecord using a single form (Ruby on Rails 5)

UPDATE: As opposed to previous versions, Rails 5 now makes it required that in a parent-child belongs_to relationship, the associated id of the parent must be present by default upon saving the child. Otherwise, there will be a validation error. And apparently it isn't allowing you to save the parent and child all in one step... So for the below solution to work, a fix would be to add optional: true to the belongs_to association in the Address model:

class Address < ApplicationRecord
belongs_to :user, optional: true
end

See my answer in a question that branched off from this one:

https://stackoverflow.com/a/39688720/5531936


It seems to me that you are mixing up the singular and plural of your address object in such a way that is not in accordance with Rails. If a User has many addresses, then your Model should show has_many :addresses and accepts_nested_attributes_for should have addresses:

class User < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses
end

and your strong params in your controller should have addresses_attributes:

def user_params
params.require(:user).permit(:name, addresses_attributes: [:id, :address])
end

Now if you want the User to just save One Address, then in your form you should have available just one instance of a nested address:

def new
@user = User.new
@user.addresses.build
end

By the way it seems like your form has fields_for when it should be f.fields_for:

<%= f.fields_for :addresses do |u| %>
<div class="field">
<%= u.label :address %>
<%= u.text_field :address %>
</div>
<% end %>

I highly recommend that you take a look at the Rails guide documentation on Nested Forms, section 9.2. It has a similar example where a Person has_many Addresses. To quote that source:

When an association accepts nested attributes fields_for renders its
block once for every element of the association. In particular, if a
person has no addresses it renders nothing. A common pattern is for
the controller to build one or more empty children so that at least
one set of fields is shown to the user. The example below would result
in 2 sets of address fields being rendered on the new person form.

def new
@person = Person.new
2.times { @person.addresses.build}
end

Rollback when creating object with nested attributes

I am assuming you are using rails 5, which changed the belongs_to relation as by default required. Which theoretically when saving nested attributes should not be a problem, but because the report-id is not yet set when saving (actually: when validating), the save will fail. This can simply be cured by telling rails how associations are related:

has_many :option_students, inverse_of: :report

Alternatively you could add optional option in the OptionsStudent class:

belongs_to :report, optional: true 

which is not as correct, it will just skip the validation, but maybe it could be relevant for the other two relations --if either the student or option is not always required.



Related Topics



Leave a reply



Submit