Rails单次表单提交批量创建多条UserIngredient记录方法
批量添加用户持有原料实现方案
别用nested_resources,那套是用来同时创建父记录+关联子记录的,你当前场景只是给已存在的当前用户批量绑定已存在的原料记录,属于多对多关联的批量绑定,直接用多选传ID数组+批量插入就行,改动量极小,还能复用你已经写好的TomSelect搜索组件。
1. 前端改造:开启多选,一次选完所有原料
你之前写的selectIngredient控制器里已经配了TomSelect的多选插件,直接把表单改成支持数组提交就行。
首先修改app/views/user_ingredients/_form.html.erb,把单条记录的表单改成批量提交表单,select加multiple属性:
<div class="w-full mx-auto"> <%= form_for :bulk_ingredients, url: user_ingredients_path, method: :post do |f| %> <div class="flex-row gap--md"> <%= f.select( :ingredient_ids, [], { prompt: '输入关键词搜索原料,可连续选择多个' }, { id: "drDwn_ingredient", class: "w-full border border-black", multiple: true, required: true, data: { controller: "selectIngredient", selectIngredient_url_value: autocomplete_ingredients_path, }, }) %> <div class="flex-row gap--xxxs"> <label> <input type="submit" class="add_cancel_ing gap--md" value="确认添加" /> <%= inline_svg_tag "svg/circle-check.svg", class: "svg_add_ing" %> </label> <%= link_to user_ingredients_path do %> <%= inline_svg_tag "svg/circle-xmark.svg", class: 'svg_cancel_ing' %> <% end %> </div> </div> <% end %> </div>
然后改app/views/user_ingredients/new.html.erb,把turbo_frame的标识改成和批量操作匹配,避免和单条记录的frame冲突:
<div> <%= turbo_frame_tag :bulk_add_ingredients do %> <%= render "form" %> <% end %> </div>
最后微调下TomSelect的配置,之前的valueField写的不对,改成和Ingredient模型主键、名称字段对应,优化多选交互:
import { Controller } from "@hotwired/stimulus"; import { get } from "@rails/request.js"; import TomSelect from "tom-select"; export default class extends Controller { static values = { url: String }; multi_select_config = function () { return { plugins: ["remove_button", "no_active_items"], valueField: "id", labelField: "name", searchField: "name", load: (q, callback) => this.search(q, callback), closeAfterSelect: false, // 多选时选完一个不自动关闭下拉,方便连续选 persist: false, create: false, }; }; async search(q, callback) { const response = await get(this.urlValue, { query: { q: q }, responseKind: "json", }); if (response.ok) { const list = await response.json; callback(list); } else { console.log("原料搜索失败", response); callback(); } } connect() { new TomSelect(this.element, this.multi_select_config()); } }
注意:确保你的autocomplete_ingredients_path接口返回的JSON格式是[{id: 原料ID, name: 原料名称}, ...]结构,和TomSelect配置的字段对应即可。
2. 后端改造:批量插入关联记录,自动去重
修改app/controllers/user_ingredients_controller.rb的create逻辑,接收ID数组,过滤掉已经添加过的原料,批量插入,比循环save性能高一个量级:
class UserIngredientsController < ApplicationController before_action :authenticate_user! before_action :set_user_ingredient, only: [:show, :destroy] def index @user_ingredients = current_user.user_ingredients.includes(:ingredient) end def new end def create # 拿到提交的原料ID数组,过滤空值转整数 submitted_ids = bulk_params[:ingredient_ids].reject(&:blank?).map(&:to_i) # 查出当前用户已经添加过的原料ID,自动去重 existing_ids = current_user.user_ingredients.where(ingredient_id: submitted_ids).pluck(:ingredient_id) valid_ids = submitted_ids - existing_ids if valid_ids.any? # 构造批量插入的数据结构 insert_data = valid_ids.map do |ing_id| { user_id: current_user.id, ingredient_id: ing_id, created_at: Time.current, updated_at: Time.current } end UserIngredient.insert_all(insert_data) flash[:notice] = "成功添加#{valid_ids.size}种原料到你的酒架" else flash[:alert] = "未选择有效原料,请勿重复添加已存在的原料" end respond_to do |format| format.html { redirect_to user_ingredients_path } format.turbo_stream { flash.now[:notice] = flash[:notice] || flash[:alert] } end end def destroy @user_ingredient.destroy respond_to do |format| format.html { redirect_to user_ingredients_path, notice: "已移除该原料" } format.turbo_stream { flash.now[:notice] = "已移除该原料" } end end private def set_user_ingredient @user_ingredient = current_user.user_ingredients.find(params[:id]) end # 允许接收数组类型的参数 def bulk_params params.require(:bulk_ingredients).permit(ingredient_ids: []) end end
3. 必做优化:加数据库唯一索引防脏数据
多对多关联表必须加联合唯一索引,防止并发请求导致同一个用户重复添加同一种原料:
生成迁移文件执行:
class AddUniqueIndexToUserIngredients < ActiveRecord::Migration[7.0] def change add_index :user_ingredients, [:user_id, :ingredient_id], unique: true end end
同时在app/models/user_ingredient.rb加模型层校验:
class UserIngredient < ApplicationRecord belongs_to :user belongs_to :ingredient validates :user, presence: true validates :ingredient, presence: true validates :ingredient_id, uniqueness: { scope: :user_id, message: "已添加过该原料" } end
改完之后用户就可以在搜索框里连续搜索、选择多个原料,点一次提交按钮全部入库,不需要每加一个点一次提交,交互流畅,代码改动量也很小,完全适配你现有的Hotwire+Stimulus+TomSelect技术栈。
内容的提问来源于stack exchange,提问作者zdebyman
相关产品推荐
相关产品推荐

