【问题标题】:How to save a nested resource in ActiveRecord using a single form (Ruby on Rails 5)如何使用单个表单在 ActiveRecord 中保存嵌套资源(Ruby on Rails 5)
【发布时间】:2017-02-02 14:38:36
【问题描述】:

我有两个具有多对一关系的实体。用户有许多地址。创建用户时,我希望表单也创建一个地址。实体是嵌套的。

方法一: 下面的代码有效,但只保存了用户,没有关联的地址。

看了一圈,我认为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) 显式创建地址。

当使用调试器跟踪此方法时,地址实体确实已成功创建,但 respond_to do 引发了 ArgumentError,原因我不明白(“respond_to 采用类型或块,从不采用两者”),这会在点击保存方法之前回滚所有内容...

[编辑] - respond_to do 引发错误是一个红鲱鱼 - 我误解了调试器。但是,由于我不明白的原因,事务被回滚了。

问题:

  1. 对于 Rails,一种或另一种方法更标准吗? (或者也许两者都不是,我从根本上误解了一些东西)
  2. 我在这两种方法中的任何一种/两种方法中做错了什么,以及如何修复它们以保存用户和地址?

下面的相关代码(它实现了上面的方法 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 错误禁止保存此用户:

地址用户必须存在

地址用户不能为空

但是,使用 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 进行测试,首先保存用户,然后添加地址工作(两条记录都写入数据库,尽管显然是在两个单独的事务中):

> 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!

【问题讨论】:

    标签: ruby-on-rails ruby activerecord ruby-on-rails-5 nested-resources


    【解决方案1】:

    更新:与之前的版本不同,Rails 5 现在要求在父子关系 belongs_to 中,在保存子节点时,父节点的关联 id 必须默认存在.否则,将出现验证错误。显然,它不允许您一次性保存父项和子项...因此,要使以下解决方案起作用,解决方法是将optional: true 添加到地址模型中的belongs_to 关联中:

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

    在一个从这个问题分支出来的问题中查看我的答案:

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


    在我看来,您以不符合 Rails 的方式混淆了 address 对象的单数和复数。如果一个用户有很多地址,那么你的模型应该显示has_many :addressesaccepts_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 地址。引用该来源:

    当一个关联接受嵌套属性 fields_for 呈现它的 为关联的每个元素阻塞一次。特别是,如果一个 人没有地址,它什么也没有。一个常见的模式是 控制器构建一个或多个空的孩子,这样至少 一组字段显示给用户。下面的示例将导致 在新人员表单上呈现的 2 组地址字段中。

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

    【讨论】:

    • 谢谢,我已经按照您的建议进行了更改,感觉离我越来越近了。不幸的是,它仍然无法正常工作。在创建用户后使用 byebug 我可以看到创建了用户和地址:#&lt;User id: nil, name: "test", created_at: nil, updated_at: nil&gt;#&lt;ActiveRecord::Associations::CollectionProxy [#&lt;Address id: nil, user_id: nil, address: "test", created_at: nil, updated_at: nil&gt;]&gt;。但是,当调用save 时,事务会回滚。我正在尝试使用 byebug 进行追踪,但最终陷入了 ActiveRecord 核心类的兔子洞......
    • 我在您的地址模型中发现了一个错误。请参阅编辑后的答案。希望这能解决问题..
    • 对不起,我认为这只是问题中的一个错字,在我的来源中实际上并不是这样。作为测试,我做了一个最简单的嵌套示例,我可以只使用模型(没有任何表单或控制器)并尝试使用 rails 控制台创建和保存,但它仍然无法正常工作......!我将此作为一个单独的问题提出,因为我在第一次尝试时有很多想法错误,并且用一个新的例子更容易解释:link
    • 啊,好吧,已经很接近解决这个问题了哈哈
    • 当(或者如果!)我找到了根本原因,我相信这将是与this 相同的问题,然后我将评论/编辑您的答案并将其标记为正确,因为您已经纠正了我上面所做的一些错误的事情!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 2011-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多