【问题标题】:How to submit multiple NEW items via Rails 3.2 mass-assignment如何通过 Rails 3.2 批量分配提交多个新项目
【发布时间】:2012-07-12 06:02:57
【问题描述】:

我有一个非常标准的用例。我有一个父对象和一个子对象列表。我想要一个表格形式,我可以一次编辑所有子项,作为表格中的行。我还希望能够插入一个或多个 new 行,并在提交时将它们创建为新记录。

当我使用 fields_for 为 has-many 相关的嵌套记录呈现一系列子表单时,rails 会生成字段名称,例如parent[children_attributes][0][fieldname]、parent[children_attributes][1][fieldname] 等等。

这会导致 Rack 解析参数哈希,如下所示:

{ "parent" => { 
    "children" => {
      "0" => { ... },
      "1" => { ... } } }

当传递一个 new(非持久化)对象时,相同的 fields_for 将生成一个如下所示的字段名称:

parent[children_attributes][][fieldname]

请注意其中没有索引的[]。

此不能与包含 [0]、[1] 等的字段以相同的形式发布,因为 Rack 感到困惑并引发了

TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams)

“好的”,我想。“我将确保所有字段使用[] 表单而不是[index] 表单。但我不知道如何说服fields_for 始终如一地这样做。即使我给它一个明确的字段名称前缀和对象:

fields_for 'parent[children_attributes][]', child do |f| ...

只要保持child,它就会自动修改字段名,使它们变成例如parent[children_attributes][0][fieldname],同时将新记录的字段名保留为 parent[children_attributes][][fieldname]。再一次,Rack barfs。

我很茫然。我到底如何使用像fields_for 这样的标准 Rails 助手来提交多个 new 记录以及现有记录,将它们解析为参数中的数组,并让所有缺少 ID 的记录在数据库中创建为新记录?我运气不好,只需要手动生成所有字段名称吗?

【问题讨论】:

  • 那么您对 ​​fields_for 的调用是什么样的?
  • 您使用的是哪个机架版本?它在以前的版本中也被破坏了吗?
  • 不是最优雅的解决方案,但您可以使用:child_index 选项并为每条新记录传递一些随机索引。请参阅this 示例。这是关于在客户端为新记录添加字段,但您应该明白这一点。不过,奇怪的是它在服务器端对您不起作用。

标签: ruby-on-rails ruby-on-rails-3


【解决方案1】:

正如其他人所提到的,[] 应该包含新记录的键,否则它会将散列与数组类型混合。您可以使用 fields_for 上的 child_index 选项进行设置。

f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...

我通常使用object_id 来执行此操作,以确保它是唯一的,以防有多个新项目。

item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...

这是一个执行此操作的抽象辅助方法。这假设有一个名称为 item_fields 的部分将呈现。

def link_to_add_fields(name, f, association)
  new_object = f.object.send(association).klass.new
  id = new_object.object_id
  fields = f.fields_for(association, new_object, child_index: id) do |builder|
    render(association.to_s.singularize + "_fields", f: builder)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

你可以这样使用它。参数是:链接名称、父表单构建器以及父模型上的关联名称。

<%= link_to_add_fields "Add Item", f, :items %>

这里有一些 CoffeeScript 来监听该链接的点击事件,插入字段,并用当前时间更新对象 id 以给它一个唯一的键。

jQuery ->
  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

该代码取自this RailsCasts Pro episode,需要付费订阅。但是,有一个免费的完整工作示例on GitHub。

更新:我想指出插入child_index 占位符并不总是必要的。如果您不想使用 JavaScript 动态插入新记录,您可以提前构建它们:

def new
  @project = Project.new
  3.times { @project.items.build }
end

<%= f.fields_for :items do |builder| %>

Rails 会自动为新记录插入索引,因此它应该可以正常工作。

【讨论】:

  • 这是我经常看到的答案,我真的很困扰使用像 object_id 这样的伪随机数作为区分符。更不用说:在面向客户端的界面中公开内部对象 ID?可怕的!如此多的攻击是基于使用无意中暴露的内部结构。真正困扰我的是 Rails 有一个非常好的方法来提交参数数组:'[]'。如果你可以强迫它单独留下'[]',这很好用!它只是非常努力地不让您在所有字段名称中使用“[]”。
  • object_id 不是必需的实现,您可以在那里使用任何随机值:id = SecureRandom.hex。但是,这并不能保证它是独一无二的。也许 object_id 的哈希会更好?
  • 我同意在fields_for 中传入[] 时插入id 的行为很奇怪。 AFAIK 自从首次添加 fields_for 以来就是这样。您可以在 3:00 左右在this episode 中看到。
  • @Avdi,我理解您的烦恼,但是一旦您开始进行更深层次的嵌套,您就需要一种将子字段与其父模型相关联的方法。想想Exam has many questions, question has many possible answers,在一种形式中,您想为每个问题创建许多问题和许多答案,因此答案字段的名称必须是exam[questions_attributes][123][answers_attributes][321][answer_text],问题的名称必须是exam[questions_attributes][123][question_text]。请注意,伪 id (123) 必须与问题及其相关答案相对应。
  • P.S.我仍然认为这对新手来说太混乱了。 “哦,你需要在 JS 中修改字段名称”似乎不是一种非常包含电池的方式来处理这种相当常见的情况:-/
【解决方案2】:

因此,我对最常看到的解决方案并不满意,即在服务器或客户端 JS 中为新元素生成伪索引。这感觉像是一个杂物,特别是考虑到 Rails/Rack 完全能够解析项目列表,只要它们都使用空括号 ([]) 作为索引。这是我最终得到的代码的近似值:

# note that this is NOT f.fields_for.
fields_for 'parent[children_attributes][]', child, index: nil do |f|
  f.label :name
  f.text_field :name
  # ...
end

以[] 结尾的字段名前缀,加上index: nil 选项,禁用索引生成Rails 因此有助于尝试提供持久对象。此 sn-p 适用于新对象和已保存对象。生成的表单参数,因为它们始终使用[],被解析为params 中的一个数组:

params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]

accepts_nested_attributes_for :children 生成的 Parent#children_attributes= 方法可以很好地处理这个数组,更新更改的记录,添加新记录(缺少 "id" 键的记录),并删除带有 "_destroy" 键集的记录。

Rails 让这件事变得如此困难,我仍然感到困扰,我不得不恢复为硬编码的字段名称前缀字符串,而不是使用例如f.fields_for :children, index: nil。作为记录,即使执行以下操作:

f.fields_for :children, index: nil, child_index: nil do |f| ...

...未能禁用字段索引生成。

我正在考虑编写一个 Rails 补丁来简化此操作,但我不知道是否有足够多的人关心它,或者它是否会被接受。

编辑:用户@Macario 告诉我为什么 Rails 更喜欢在字段名称中使用显式索引:一旦您进入三层嵌套模型,就需要有一种方法来区分哪个二级模型和三级属性属于。

【讨论】:

  • 我已经多次遇到这个问题,最后我总是自己生成字段名称。如果它在那里并且有效,我肯定会使用该选项。
  • 这里也一样。每次我需要在 Rails 中做主子表格时,我都在努力解决这个问题。
  • 我相信 Rails 更喜欢哈希而不是数组的原因是为了让多个属性可靠地工作:foo[bar][][zap] 和 foo[bar][][zip] 需要与同一条记录相关联。当有深度嵌套的记录时,这个问题会被放大。 Macario 在另一条评论中提到了这一点。
  • AFAICT,如果孩子的顺序不确定,这将导致问题。例如,在 has-many 关系中,除非您总是按同一列对子项进行排序,否则您的关系数据库可能会以意外的顺序返回子项。这将导致错误的参数值应用于子级。
  • @ryanb 感谢您的澄清。在我的测试中似乎工作正常;我认为它是根据订单工作的。但是,我现在注意到 Macario 对一种形式的多重嵌套模型的警告。
【解决方案3】:

常见的解决方案是在 [] 中添加一个占位符,并在将 sn-p 插入表单时将其替换为唯一编号。时间戳在大部分时间都有效。

【讨论】:

  • 时间戳在大多数情况下都有效?...那是在要求一个很难重新创建的错误,不是吗?
  • 他的意思是“对于大多数用例”,而不是“意外失败”。
【解决方案4】:

也许你应该作弊。将新记录放在一个不同的虚假属性中,该属性是实际记录的装饰器。

parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]

它不漂亮,但它应该可以工作。支持表单往返验证错误可能需要一些额外的努力。

【讨论】:

  • 这让我大吃一惊,我需要这样做。而且,事实上,我可以发誓过去我不需要这样做。
  • 哦,还有:accepts_nested_attributes_for-generated setter 接受多个新的(无 ID)属性哈希并为所有这些哈希创建新记录同时保存更新记录的能力是明确的记录在案,表明这应该是完全可行的。
【解决方案5】:

我在最近的所有项目中都遇到过这个用户案例,我希望这种情况会继续下去,正如 julian7 指出的那样,有必要在 [] 中提供一个唯一的 ID。在我看来,这最好通过 js 完成。我一直在拖动和改进一个 jquery 插件来处理这种情况。它适用于现有记录并用于添加新记录,但需要一定的标记并且它会优雅地降级,这是代码和示例:

https://gist.github.com/3096634

使用插件的注意事项:

  1. fields_for 调用应包含在 &lt;fieldset&gt; 中,其 data-association 属性等于模型的复数名称,以及一个类“nested_models”。

  2. 应该在调用 fields_for 之前在视图中构建一个对象。

  3. 对象字段本身应包含在 &lt;fieldset&gt; 中,类为“new”,但前提是记录是新的(不记得我是否删除了此要求)。

  4. 标签内“_destroy”属性的复选框必须存在,插件将使用标签文本创建销毁链接。

  5. “add_record”类的链接应该存在于 fieldset.nested_models 中,但在包含模型字段的字段集之外。

除此之外,它一直在为我创造奇迹。
在检查了要点之后,这个要求必须更清楚。 如果您改进代码或使用它,请告诉我:)。
顺便说一句,我受到 Ryan Bates 的第一个嵌套模型截屏视频的启发。

【讨论】:

    【解决方案6】:

    长帖已删除

    Ryan 对此有一个插曲: http://railscasts.com/episodes/196-nested-model-form-revised

    您似乎需要手动生成唯一索引。 Ryan 为此使用了object_id。

    【讨论】:

    • 那么就删除你的答案。
    • 删除链接由于某种原因不存在。我再次更新了帖子。
    【解决方案7】:

    我认为您可以通过将记录的 id 包含为隐藏字段来使其工作

    【讨论】:

    • 新记录还没有 id。
    • 因此,对于具有相同效果的那些,您将传递 nil。
    【解决方案8】:

    有一个叫做茧的宝石可以做到这一点,我会选择更精简的 mor DIY 方法,但它是专门为这种情况而构建的。

    【讨论】:

      猜你喜欢
      • 2012-04-01
      • 2012-06-27
      • 2012-12-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-06
      • 1970-01-01
      相关资源
      最近更新 更多