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

☆樱花仙子☆ 提交于 2021-01-04 09:14:15

问题


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!


回答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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!