You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:57:16