问题
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:
- Is one or the other approach more standard for Rails? (or maybe neither are and I'm fundamentally misunderstanding something)
- 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!
回答1:
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
来源:https://stackoverflow.com/questions/39682727/how-to-save-a-nested-resource-in-activerecord-using-a-single-form-ruby-on-rails