Rails使用Cocoon嵌套字段时link_to_add_association编辑误删关联记录
Rails嵌套属性场景下编辑父记录时关联数据被意外删除问题排查
问题背景
- 关联层级:
Setup关联1个Material,Material关联1个RotapAnalysis,RotapAnalysis关联多个RotapSieve - 异常触发条件:仅编辑已存在、且已绑定RotapAnalysis的Setup记录时会触发关联数据删除;新建Setup、编辑时手动新建RotapAnalysis的场景完全正常
- 涉及能力:Rails原生
nested fields+accepts_nested_attributes_for,搭配动态添加嵌套字段的link_to_add_association辅助方法
触发异常的视图代码
<%= f.fields_for :rotap_analysis do |ra| %> <%= render 'materials/rotap_analysis_fields', f: ra %> <% end %> <div class="links float-e-margins"> <!-- 此处代码触发rotap analysis删除 --> <%= link_to_add_association '+ rotap analysis', f, :rotap_analysis, partial: 'materials/rotap_analysis_fields', class: "btn btn-info btn-xs" %> </div>
异常触发时的服务端日志特征
日志显示页面渲染阶段(未到用户提交表单更新数据的步骤)就直接开启事务,删除了关联的RotapSieve和RotapAnalysis记录:
RotapAnalysis Load (0.3ms) SELECT "rotap_analyses".* FROM "rotap_analyses" WHERE "rotap_analyses"."material_id" = $1 LIMIT $2 [["material_id", 23], ["LIMIT", 1]] ↳ app/views/materials/_material_fields.html.erb:77 RotapSieve Load (4.2ms) SELECT "rotap_sieves".* FROM "rotap_sieves" WHERE "rotap_sieves"."rotap_analysis_id" = $1 [["rotap_analysis_id", 18]] ↳ app/views/materials/_rotap_analysis_fields.html.erb:24 Rendered materials/_rotap_sieve_fields.html.erb (Duration: 1.0ms | Allocations: 488) Rendered materials/_rotap_sieve_fields.html.erb (Duration: 0.9ms | Allocations: 475) Rendered materials/_rotap_analysis_fields.html.erb (Duration: 13.0ms | Allocations: 2823) TRANSACTION (0.3ms) BEGIN ↳ app/views/materials/_material_fields.html.erb:82 RotapSieve Destroy (0.4ms) DELETE FROM "rotap_sieves" WHERE "rotap_sieves"."id" = $1 [["id", 17]] ↳ app/views/materials/_material_fields.html.erb:82 RotapAnalysis Destroy (0.5ms) DELETE FROM "rotap_analyses" WHERE "rotap_analyses"."id" = $1 [["id", 18]] ↳ app/views/materials/_material_fields.html.erb:82 TRANSACTION (1.0ms) COMMIT ↳ app/views/materials/_material_fields.html.erb:82 Rendered materials/_rotap_sieve_fields.html.erb (Duration: 0.9ms | Allocations: 475) Rendered materials/_rotap_analysis_fields.html.erb (Duration: 4.2ms | Allocations: 1495) Rendered materials/_material_fields.html.erb (Duration: 43.9ms | Allocations: 12427) Rendered setups/_form.html.erb (Duration: 123.8ms | Allocations: 42184) Rendered setups/edit.html.erb within layouts/application (Duration: 124.1ms | Allocations: 42245)
根因定位
该问题和嵌套层级无直接关联,核心是has_one关联搭配accepts_nested_attributes_for的默认行为,和动态添加字段辅助方法的执行逻辑冲突导致:
Material与RotapAnalysis为has_one关联,给Material配置accepts_nested_attributes_for :rotap_analysis时,Rails默认配置:update_only为false——只要传入的嵌套属性未携带原有记录的id,就会先删除旧关联记录,再尝试构建新记录。- 页面中的
link_to_add_association渲染时,会提前实例化一个新的RotapAnalysis对象挂载到Material的rotap_analysis关联上。对于has_one关联,一旦给关联属性赋新值,Rails的自动保存机制会直接将原有持久化的关联对象标记为待删除,在对象持久化时直接执行DELETE语句,这也是未提交表单、仅渲染页面就触发删除事务的原因。 - 新建记录场景不触发异常,是因为不存在已持久化的原关联记录;编辑时手动新建RotapAnalysis的场景不触发,是因为原记录本来就没有绑定过RotapAnalysis,无旧记录可删。
解决方案
按优先级选择任意一种即可:
- 方案1(推荐):给
has_one对应的嵌套属性配置加上update_only: true,修改Material模型的accepts_nested_attributes_for配置:
该配置会告知Rails:如果嵌套属性对应已存在的关联记录,直接更新原记录,不执行先删后建逻辑,也不会因为关联上挂载了新的临时对象就删除原有持久化记录。class Material < ApplicationRecord has_one :rotap_analysis accepts_nested_attributes_for :rotap_analysis, update_only: true end - 方案2:调整
link_to_add_association的渲染逻辑,不要在页面渲染阶段直接给父关联挂载新实例对象,增加参数控制新对象的构建时机,仅在用户点击添加按钮时才实例化新的RotapAnalysis。 - 方案3:给
has_one关联配置autosave: false,关闭关联自动保存逻辑,但该方案会影响其他正常的嵌套属性更新场景,不优先选用。
内容的提问来源于stack exchange,提问作者karns
相关产品推荐
相关产品推荐

