asibs asibs - 2 months ago 6
Ruby Question

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

I have two entities with a many-to-one relationship. User has many Addresses. When creating a User I want the form to also create a single Address. The entities are nested.

Approach 1:
The code below works, but only saves the User, no associated Address.

Reading around, I thought that the

accepts_nested_attributes_for
would automatically save the address. I'm not sure, but it may be that this isn't working because the parameters I'm getting into the Controller don't actually appear to be nested, ie. they look like:

"user"=>{"name"=>"test"}, "address"=>{"address"=>"test"}


Rather than being nested like this:

"user"=>{"name"=>"test", "address"=>{"address"=>"test"} }


I assume this could be due to something wrong in my form, but I don't know what the problem is...

Approach 2:
I have also tried changing the controller - implementing a second private method,
address_params
, which looked like
params.require(:address).permit(:address)
, and then explicitly creating the address with
@user.address.build(address_params)
in the
create
method.

When tracing through this approach with a debugger the Address entity did indeed get created successfully, however the
respond_to do
raised an ArgumentError for reasons I don't understand ("respond_to takes either types or a block, never both"), and this rolls everything back before hitting the save method...

[EDIT] - The
respond_to do
raising an error was a red herring - I was misinterpreting the debugger. However, the transaction is rolled back for reasons I don't understand.

Questions:


  1. Is one or the other approach more standard for Rails? (or maybe neither are and I'm fundamentally misunderstanding something)

  2. What am I doing wrong in either / both of these approaches, and how to fix them so both User and Address are saved?



Relevant code below (which implements Approach 1 above, and generates the non-nested params as noted):

user.rb

class User < ApplicationRecord
has_many :address
accepts_nested_attributes_for :address
end


address.rb

class Address < ApplicationRecord
belongs_to :user
end


users_controller.rb

class UsersController < ApplicationController
# GET /users/new
def new
@user = User.new
end

# POST /users
# POST /users.json
def create
@user = User.new(user_params)

respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: 'User was successfully created.' }
format.json { render :show, status: :created, location: @user}
else
format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end

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

end


_form.html.erb

<%= form_for(user) do |f| %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>

<%= fields_for(user.address.build) do |u| %>
<div class="field">
<%= u.label :address %>
<%= u.text_field :address %>
</div>
<% end %>

<div class="actions">
<%= f.submit %>
</div>
<% end %>


UPDATE 1:

After making the changes suggested by @Ren, I can see that the parameters look more like what I would've expected for nested resources:

"user"=>{"name"=>"test", "addresses_attributes"=>{"0"=>{"address"=>"test"}}}


However, when trying to save the user, the transaction is still rolled back for reasons I don't understand. The output I get from the users.new page is:


2 error prohibited this user from being saved:

Addresses user must exist

Addresses user can't be blank


However, using byebug, after the
@user = User.new(user_params)
call, things look as I would expect them:

(byebug) @user
#<User id: nil, name: "test", created_at: nil, updated_at: nil>
(byebug) @user.addresses
#<ActiveRecord::Associations::CollectionProxy [#<Address id: nil, user_id: nil, address: "test", created_at: nil, updated_at: nil>]>


Obviously the user.id field is not set until the record is written to the DB, so equally the address.user_id field cannot be set until user is saved, so maybe this is caused by some sort of incorrect ordering when ActiveRecord is saving to the database? I will continue to try to understand what's going on by debugging with byebug...

UPDATE 2:

Using
rails console
to test, saving User first and then adding the Address works (both records get written to the DB, although obviously in 2 separate transactions):

> user = User.new(name: "consoleTest")
> user.save
> user.addresses.build(address: "consoleTest")
> user.save


Saving only once at the end results in the same issues I'm seeing when running my program, ie. the transaction is rolled back for some reason:

> user = User.new(name: "consoleTest")
> user.addresses.build(address: "consoleTest")
> user.save


As far as I can tell from debugging with
rails console
, the only difference between the state of
user.addresses
in these two approaches is that in the first
address.user_id
is already set, since the
user.id
is already known, while as in the second, it is not. So this may be the problem, but from what I understand, the
save
method should ensure entities are saved in the correct order such that this is not a problem. Ideally it would be nice to be able to see which entities
save
is trying to write to the DB and in which order, but debugging this with byebug takes me down an ActiveRecord rabbit-hold I don't understand at all!

Ren Ren
Answer

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:

http://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