如何使用单个表单在 ActiveRecord 中保存嵌套资源(Ruby on Rails 5)
How to save a nested resource in ActiveRecord using a single form (Ruby on Rails 5)
我有两个具有多对一关系的实体。用户有很多地址。创建用户时,我希望表单也创建一个地址。实体是嵌套的。
方法一:
下面的代码有效,但只保存了用户,没有关联的地址。
看了一圈,以为accepts_nested_attributes_for
会自动保存地址。我不确定,但这可能不起作用,因为我进入控制器的参数实际上并没有嵌套,即。它们看起来像:
"user"=>{"name"=>"test"}, "address"=>{"address"=>"test"}
而不是像这样嵌套:
"user"=>{"name"=>"test", "address"=>{"address"=>"test"} }
我想这可能是因为我的表格有问题,但我不知道问题出在哪里...
方法二:
我还尝试更改控制器 - 实现第二个私有方法 address_params
,它看起来像 params.require(:address).permit(:address)
,然后在 create
方法中使用 @user.address.build(address_params)
显式创建地址。
当使用调试器跟踪这种方法时,Address 实体确实成功创建,但是 respond_to do
引发了一个 ArgumentError,原因我不明白(“respond_to 采用类型或一个块,永远不会两者都有”),这会在点击保存方法之前回滚所有内容...
[编辑] - respond_to do
引发错误是一个转移注意力的问题 - 我误解了调试器。但是,由于我不明白的原因,交易被回滚了。
问题:
- 对于Rails,一种方法还是另一种方法更标准? (或者两者都不是,我从根本上误解了一些东西)
- 我在这两种方法中做错了什么,以及如何修复它们以便保存用户和地址?
下面的相关代码(实现上面的方法 1,并生成非嵌套参数,如前所述):
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 %>
更新 1:
按照@Ren 的建议进行更改后,我可以看到参数看起来更像我对嵌套资源的预期:
"user"=>{"name"=>"test", "addresses_attributes"=>{"0"=>{"address"=>"test"}}}
但是,当试图保存用户时,由于我不明白的原因,事务仍然被回滚。我从 users.new 页面得到的输出是:
2 error prohibited this user from being saved:
Addresses user must exist
Addresses user can't be blank
然而,使用 byebug,在 @user = User.new(user_params)
调用之后,事情看起来和我预期的一样:
(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>]>
显然 user.id 字段在将记录写入数据库之前不会设置,因此同样地 address.user_id 字段在用户保存之前无法设置,所以这可能是由某种原因引起的ActiveRecord 保存到数据库时顺序不正确?我将继续尝试通过使用 byebug 进行调试来了解发生了什么...
更新 2:
使用 rails console
进行测试,先保存用户,然后添加地址(两条记录都写入数据库,尽管显然是在 2 个单独的事务中):
> user = User.new(name: "consoleTest")
> user.save
> user.addresses.build(address: "consoleTest")
> user.save
最后只保存一次会导致我在 运行 我的程序中看到的相同问题,即。事务因某种原因被回滚:
> user = User.new(name: "consoleTest")
> user.addresses.build(address: "consoleTest")
> user.save
据我通过 rails console
调试可以看出,这两种方法中 user.addresses
的状态之间的唯一区别是第一个 address.user_id
已经设置,因为 user.id
是已知的,而在第二个中,它不是。所以这可能是问题所在,但据我了解,save
方法应该确保实体以正确的顺序保存,这样这就不是问题。理想情况下,能够看到哪些实体 save
正在尝试写入数据库以及以何种顺序写入数据库会很好,但是使用 byebug 进行调试会让我陷入 ActiveRecord rabbit-hold 我根本不明白!
更新:与以前的版本相反,Rails 5 现在要求在 parent-child belongs_to
关系中,关联的在保存 child 时,parent 的 ID 必须默认存在。否则,将出现验证错误。而且 apparently 它不允许您一步保存 parent 和 child ...因此,为了使下面的解决方案起作用,解决方法是添加 optional: true
到地址模型中的 belongs_to
关联:
class Address < ApplicationRecord
belongs_to :user, optional: true
end
请参阅我在从这个问题分支出来的问题中的回答:
在我看来,您混淆了 address
object 的单数和复数,这与 Rails 不一致。如果一个用户 有很多 个地址,那么你的模型应该显示 has_many :addresses
而 accepts_nested_attributes_for
应该有 addresses
:
class User < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses
end
你控制器中的强参数应该有 addresses_attributes
:
def user_params
params.require(:user).permit(:name, addresses_attributes: [:id, :address])
end
现在,如果您希望用户只保存一个地址,那么在您的表单中您应该只有一个可用的嵌套地址实例:
def new
@user = User.new
@user.addresses.build
end
顺便说一下,您的表单似乎有 fields_for
,而它应该是 f.fields_for
:
<%= f.fields_for :addresses do |u| %>
<div class="field">
<%= u.label :address %>
<%= u.text_field :address %>
</div>
<% end %>
我强烈建议您查看 Rails guide 文档中有关嵌套表单的第 9.2 节。它有一个类似的例子,其中一个人 has_many
地址。引用该来源:
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
我有两个具有多对一关系的实体。用户有很多地址。创建用户时,我希望表单也创建一个地址。实体是嵌套的。
方法一: 下面的代码有效,但只保存了用户,没有关联的地址。
看了一圈,以为accepts_nested_attributes_for
会自动保存地址。我不确定,但这可能不起作用,因为我进入控制器的参数实际上并没有嵌套,即。它们看起来像:
"user"=>{"name"=>"test"}, "address"=>{"address"=>"test"}
而不是像这样嵌套:
"user"=>{"name"=>"test", "address"=>{"address"=>"test"} }
我想这可能是因为我的表格有问题,但我不知道问题出在哪里...
方法二:
我还尝试更改控制器 - 实现第二个私有方法 address_params
,它看起来像 params.require(:address).permit(:address)
,然后在 create
方法中使用 @user.address.build(address_params)
显式创建地址。
当使用调试器跟踪这种方法时,Address 实体确实成功创建,但是 respond_to do
引发了一个 ArgumentError,原因我不明白(“respond_to 采用类型或一个块,永远不会两者都有”),这会在点击保存方法之前回滚所有内容...
[编辑] - respond_to do
引发错误是一个转移注意力的问题 - 我误解了调试器。但是,由于我不明白的原因,交易被回滚了。
问题:
- 对于Rails,一种方法还是另一种方法更标准? (或者两者都不是,我从根本上误解了一些东西)
- 我在这两种方法中做错了什么,以及如何修复它们以便保存用户和地址?
下面的相关代码(实现上面的方法 1,并生成非嵌套参数,如前所述):
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 %>
更新 1:
按照@Ren 的建议进行更改后,我可以看到参数看起来更像我对嵌套资源的预期:
"user"=>{"name"=>"test", "addresses_attributes"=>{"0"=>{"address"=>"test"}}}
但是,当试图保存用户时,由于我不明白的原因,事务仍然被回滚。我从 users.new 页面得到的输出是:
2 error prohibited this user from being saved:
Addresses user must exist
Addresses user can't be blank
然而,使用 byebug,在 @user = User.new(user_params)
调用之后,事情看起来和我预期的一样:
(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>]>
显然 user.id 字段在将记录写入数据库之前不会设置,因此同样地 address.user_id 字段在用户保存之前无法设置,所以这可能是由某种原因引起的ActiveRecord 保存到数据库时顺序不正确?我将继续尝试通过使用 byebug 进行调试来了解发生了什么...
更新 2:
使用 rails console
进行测试,先保存用户,然后添加地址(两条记录都写入数据库,尽管显然是在 2 个单独的事务中):
> user = User.new(name: "consoleTest")
> user.save
> user.addresses.build(address: "consoleTest")
> user.save
最后只保存一次会导致我在 运行 我的程序中看到的相同问题,即。事务因某种原因被回滚:
> user = User.new(name: "consoleTest")
> user.addresses.build(address: "consoleTest")
> user.save
据我通过 rails console
调试可以看出,这两种方法中 user.addresses
的状态之间的唯一区别是第一个 address.user_id
已经设置,因为 user.id
是已知的,而在第二个中,它不是。所以这可能是问题所在,但据我了解,save
方法应该确保实体以正确的顺序保存,这样这就不是问题。理想情况下,能够看到哪些实体 save
正在尝试写入数据库以及以何种顺序写入数据库会很好,但是使用 byebug 进行调试会让我陷入 ActiveRecord rabbit-hold 我根本不明白!
更新:与以前的版本相反,Rails 5 现在要求在 parent-child belongs_to
关系中,关联的在保存 child 时,parent 的 ID 必须默认存在。否则,将出现验证错误。而且 apparently 它不允许您一步保存 parent 和 child ...因此,为了使下面的解决方案起作用,解决方法是添加 optional: true
到地址模型中的 belongs_to
关联:
class Address < ApplicationRecord
belongs_to :user, optional: true
end
请参阅我在从这个问题分支出来的问题中的回答:
在我看来,您混淆了 address
object 的单数和复数,这与 Rails 不一致。如果一个用户 有很多 个地址,那么你的模型应该显示 has_many :addresses
而 accepts_nested_attributes_for
应该有 addresses
:
class User < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses
end
你控制器中的强参数应该有 addresses_attributes
:
def user_params
params.require(:user).permit(:name, addresses_attributes: [:id, :address])
end
现在,如果您希望用户只保存一个地址,那么在您的表单中您应该只有一个可用的嵌套地址实例:
def new
@user = User.new
@user.addresses.build
end
顺便说一下,您的表单似乎有 fields_for
,而它应该是 f.fields_for
:
<%= f.fields_for :addresses do |u| %>
<div class="field">
<%= u.label :address %>
<%= u.text_field :address %>
</div>
<% end %>
我强烈建议您查看 Rails guide 文档中有关嵌套表单的第 9.2 节。它有一个类似的例子,其中一个人 has_many
地址。引用该来源:
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