Rails 4不更新嵌套属性

时间:2022-07-02 02:41:58

Issue: Instead of updating nested attributes, they are being created on top of the existing nested attributes when I hit the #update action of the associated features_controller.rb

问题:当我点击相关的features_controller.rb的#update操作时,不是更新嵌套属性,而是在现有嵌套属性之上创建嵌套属性

Likely Cause: I think the problem lies in my lack of understanding in Rails' form_for. I think the breakdown is in my views, how I render the persisting nested attributes, and/or how I fail to specify the nested attribute's id, causing it to simply create a new one

可能的原因:我认为问题在于我对Rails的form_for缺乏理解。我认为问题出在我的视图中,我如何呈现持久化的嵌套属性,以及/或我如何不能指定嵌套属性的id,导致它仅仅创建一个新的id

feature.rb

feature.rb

class Feature < ActiveRecord::Base
  ...
  has_many :scenarios
  accepts_nested_attributes_for :scenarios,
    allow_destroy: true,
    reject_if: :all_blank
  ...
end

features_controller.rb

features_controller.rb

def update
  ...
  project = Project.find(params[:project_id])
  @feature = Feature.find(params[:id])

  if @feature.update_attributes(feature_params)
    # checking feature_params looks good...
    # feature_params['scenarios'] => { <correct object hash> }

    redirect_to project
  else
    render :edit
  end
end

...

private
def feature_params
  params.require(:feature).permit(:title, :narrative, :price, :eta, scenarios_attributes[:description, :_destroy])
end

_form.html.haml (simplified)

_form.html。haml(简化)

= form_for [@project, @feature] do |f|
  ...
  - if @feature.new_record? -# if we are creating new feature
    = f.fields_for :scenarios, @feature.scenarios.build do |builder|
      = builder.label :description, "Scenario"
      = builder.text_area :description, rows: "3", autocomplete: "off"

  - else -# if we are editing an existing feature
    = f.fields_for :scenarios do |builder|
      = builder.label :description, "Scenario"
      = builder.text_area :description, rows: "3", autocomplete: "off"

I'm sure there's a nicer way to achieve the if @feature.new_record? check. I'm also using a few Javascript hooks to create dynamic nested attribute forms (which I've left out), heavily influenced by Railscast #196 Nested Model Form (revised)

我确信有更好的方法来实现if @feature.new_record吗?检查。我还使用了一些Javascript钩子来创建动态的嵌套属性表单(我漏掉了),这在很大程度上受到Railscast #196嵌套模型表单的影响。

I would love a really nice Rails-y implementation of dealing with these sorts of nested forms.

我想要一个非常好的rails实现来处理这些嵌套的表单。

2 个解决方案

#1


39  

Try adding :id to the :scenario_attributes portion of your feature_params method. You only have the description field and the ability to allow a destroy.

尝试向feature_params方法的:scenario - attributes部分添加:id。你只有描述字段和允许破坏的能力。

def feature_params
  # added => before nested attributes
  params.require(:feature).permit(:id, :title, :narrative, :price, :eta, scenarios_attributes => [:id, :description, :_destroy])
end

As @vinodadhikary suggested, you no longer need to check if feature is a new record, since Rails, specifically using the form_for method, will do that for you.

正如@vinodadhikary所建议的,您不再需要检查特性是否为新记录,因为Rails(特别是使用form_for方法)将为您做这些。

Update:

更新:

You don't need to define if @feature.new_record? ... else in your form. It will be taken care by Rails when you use form_for. Rails checks if the action is going to be create or update based on object.persisted?, so, you can update your form to:

不需要定义@feature.new_record吗?…其他表单。当您使用form_for时,Rails会注意到这一点。Rails会根据objec . persistent检查动作是创建还是更新?,你可将表格更新为:

= form_for [@project, @feature] do |f|
  ...
  = f.fields_for :scenarios, @feature.scenarios.build do |builder|
    = builder.label :description, "Scenario"
    = builder.text_area :description, rows: "3", autocomplete: "off"

#2


4  

As @Philip7899 mentioned as a comment in the accepted answer, allowing the user to set the id means that they could "steal" children records belonging to another user.

正如@Philip7899在已接受的答案中提到的那样,允许用户设置id意味着他们可以“窃取”属于另一个用户的子记录。

However, Rails accepts_nested_attributes_for actually checks the id and raises:

然而,Rails accepts_nested_attributes_for实际上会检查id并提升:

ActiveRecord::RecordNotFound:
  Couldn't find Answer with ID=5 for Questionnaire with ID=5

Basically the ids are looked for in the children association (again, as said by @glampr). Therefor, the child record belonging to another user is not found.

基本上,这些id是在儿童协会中查找的(同样,@glampr说过)。因此,不会找到属于另一个用户的子记录。

Ultimately, 401 is the response status (unlike the usual 404 from ActiveRecord::RecordNotFound)

最终,401是响应状态(不同于ActiveRecord常见的404::RecordNotFound)

Follows some code I used to test the behaviour.

下面是我用来测试行为的一些代码。

let :params do
  {
    id: questionnaire.id,
    questionnaire: {
      participation_id: participation.id,
      answers_attributes: answers_attributes
    }
  }
end

let :evil_params do
  params.tap do |params|
    params[:questionnaire][:answers_attributes]['0']['id'] = another_participant_s_answer.id.to_s
  end
end

it "doesn't mess with other people's answers" do
  old_value = another_participant_s_answer.value

  put :update, evil_params

  expect(another_participant_s_answer.reload.value).to eq(old_value) # pass
  expect(response.status).to eq(401) # pass
end

In conclusion, adding the id to the permitted params as stated above is correct and safe.

综上所述,将id添加到上述允许的参数中是正确和安全的。

Fascinating Rails.

迷人的Rails。

#1


39  

Try adding :id to the :scenario_attributes portion of your feature_params method. You only have the description field and the ability to allow a destroy.

尝试向feature_params方法的:scenario - attributes部分添加:id。你只有描述字段和允许破坏的能力。

def feature_params
  # added => before nested attributes
  params.require(:feature).permit(:id, :title, :narrative, :price, :eta, scenarios_attributes => [:id, :description, :_destroy])
end

As @vinodadhikary suggested, you no longer need to check if feature is a new record, since Rails, specifically using the form_for method, will do that for you.

正如@vinodadhikary所建议的,您不再需要检查特性是否为新记录,因为Rails(特别是使用form_for方法)将为您做这些。

Update:

更新:

You don't need to define if @feature.new_record? ... else in your form. It will be taken care by Rails when you use form_for. Rails checks if the action is going to be create or update based on object.persisted?, so, you can update your form to:

不需要定义@feature.new_record吗?…其他表单。当您使用form_for时,Rails会注意到这一点。Rails会根据objec . persistent检查动作是创建还是更新?,你可将表格更新为:

= form_for [@project, @feature] do |f|
  ...
  = f.fields_for :scenarios, @feature.scenarios.build do |builder|
    = builder.label :description, "Scenario"
    = builder.text_area :description, rows: "3", autocomplete: "off"

#2


4  

As @Philip7899 mentioned as a comment in the accepted answer, allowing the user to set the id means that they could "steal" children records belonging to another user.

正如@Philip7899在已接受的答案中提到的那样,允许用户设置id意味着他们可以“窃取”属于另一个用户的子记录。

However, Rails accepts_nested_attributes_for actually checks the id and raises:

然而,Rails accepts_nested_attributes_for实际上会检查id并提升:

ActiveRecord::RecordNotFound:
  Couldn't find Answer with ID=5 for Questionnaire with ID=5

Basically the ids are looked for in the children association (again, as said by @glampr). Therefor, the child record belonging to another user is not found.

基本上,这些id是在儿童协会中查找的(同样,@glampr说过)。因此,不会找到属于另一个用户的子记录。

Ultimately, 401 is the response status (unlike the usual 404 from ActiveRecord::RecordNotFound)

最终,401是响应状态(不同于ActiveRecord常见的404::RecordNotFound)

Follows some code I used to test the behaviour.

下面是我用来测试行为的一些代码。

let :params do
  {
    id: questionnaire.id,
    questionnaire: {
      participation_id: participation.id,
      answers_attributes: answers_attributes
    }
  }
end

let :evil_params do
  params.tap do |params|
    params[:questionnaire][:answers_attributes]['0']['id'] = another_participant_s_answer.id.to_s
  end
end

it "doesn't mess with other people's answers" do
  old_value = another_participant_s_answer.value

  put :update, evil_params

  expect(another_participant_s_answer.reload.value).to eq(old_value) # pass
  expect(response.status).to eq(401) # pass
end

In conclusion, adding the id to the permitted params as stated above is correct and safe.

综上所述,将id添加到上述允许的参数中是正确和安全的。

Fascinating Rails.

迷人的Rails。