如何在Rails中通过Cocoon gem嵌套属性更新关联表多复选框值
多对多关联下批量创建Assignment记录的实现方案
场景概述
现有User、Client、Role三个模型,三者通过Assignment关联表建立多对多关系,每个Assignment记录对应一个用户、一个客户端、一个角色。当前使用Cocoon gem动态添加Assignment字段,需求是:提交用户表单时,若为同一个客户端选择多个角色,需在Assignment表中生成多条对应记录(每个角色对应一条,共享同一用户和客户端)。
现有模型代码
User 模型
class User < ApplicationRecord has_many :assignments, inverse_of: :user, dependent: :destroy has_many :roles, through: :assignments accepts_nested_attributes_for :assignments, reject_if: :all_blank, allow_destroy: true end
Client 模型
class Client < ApplicationRecord has_many :assignments, inverse_of: :client, dependent: :destroy has_many :users, through: :assignments end
Role 模型
class Role < ApplicationRecord has_many :assignments, inverse_of: :role, dependent: :destroy has_many :users, through: :assignments end
Assignment 关联表模型
class Assignment < ApplicationRecord belongs_to :user belongs_to :role belongs_to :client end
现有视图代码
form.html.erb
<%= f.fields_for :assignments, url: url do |assignment_form| %> <%= render partial: 'users/assignment_fields', locals: { f: assignment_form, user: @user } %> <% end %> <div> <%= link_to_add_association "添加", f, :assignments, partial: 'users/assignment_fields', render_options: { locals: { user: @user } } %> </div>
_assignment_fields.html.erb
<%= f.collection_check_boxes :role_id, Role.all, :id, :short_name, html_options = {} %>
参数许可配置
[:first_name, :last_name, assignments_attributes: [:client_id, role_id: [] ] ]
提交参数示例
"user"=><ActionController::Parameters {"first_name"=>"a", "last_name"=>"d", "assignments_attributes"=><ActionController::Parameters {"0"=><ActionController::Parameters {"client_id"=>"2", "role_id"=>["2", "4"], "id"=>"300"}
实现方案
1. 调整视图字段逻辑
修改_assignment_fields.html.erb,添加客户端选择框,让用户先指定客户端,再选择该客户端下的多个角色:
<div class="assignment-group"> <%= f.select :client_id, Client.all.map { |c| [c.name, c.id] }, prompt: "选择客户端", class: "form-control mb-2" %> <div class="roles-checkboxes"> <%= f.collection_check_boxes :role_id, Role.all, :id, :short_name do |b| %> <div class="form-check"> <%= b.check_box class: "form-check-input" %> <%= b.label class: "form-check-label" %> </div> <% end %> </div> <%= link_to_remove_association "移除", f, class: "btn btn-sm btn-danger mt-2" %> </div>
2. 控制器层处理参数拆分
在UsersController的参数许可方法中,将单个Assignment条目里的role_id数组拆分为多个独立的Assignment条目,每个条目对应一个角色和相同的客户端。同时处理编辑场景下的现有记录更新/删除:
def user_params params = params.require(:user).permit( :first_name, :last_name, assignments_attributes: [:id, :client_id, :role_id, :_destroy] ) return params unless params[:assignments_attributes].present? new_assignments = [] # 收集用户现有Assignment的id与对应角色ID映射 existing_assignments = @user.assignments.each_with_object({}) do |a, hash| hash[a.id.to_s] = a.role_id.to_s end params[:assignments_attributes].each do |_, assignment| client_id = assignment[:client_id] # 整理选中的角色ID,过滤空值 selected_role_ids = if assignment[:role_id].is_a?(Array) assignment[:role_id].reject(&:blank?) else [assignment[:role_id]].reject(&:blank?) end assignment_id = assignment[:id] if assignment_id.present? # 处理现有Assignment记录 existing_role_id = existing_assignments[assignment_id] if selected_role_ids.include?(existing_role_id) # 保留当前记录,移除已存在的角色ID避免重复创建 new_assignments << assignment.except(:role_id).merge(role_id: existing_role_id) selected_role_ids.delete(existing_role_id) else # 标记删除未选中的现有记录 new_assignments << assignment.merge(_destroy: '1') end end # 为剩余选中的角色创建新的Assignment条目 selected_role_ids.each do |role_id| new_assignments << { client_id: client_id, role_id: role_id } end end # 重新构建assignments_attributes的索引 params[:assignments_attributes] = new_assignments.map.with_index { |a, i| [i, a] }.to_h params end
3. 验证效果
提交表单后,原来包含role_id数组的参数会被拆分为多个独立的Assignment条目,例如:
"user"=><ActionController::Parameters { "first_name"=>"a", "last_name"=>"d", "assignments_attributes"=><ActionController::Parameters { "0"=><ActionController::Parameters {"client_id"=>"2", "role_id"=>"2", "id"=>"300"}, "1"=><ActionController::Parameters {"client_id"=>"2", "role_id"=>"4"} } }
Rails的嵌套属性机制会自动创建/更新对应的Assignment记录,实现一个客户端对应多个角色的需求。
内容的提问来源于stack exchange,提问作者Adil Tuladhar
相关产品推荐
相关产品推荐

