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

如何在Ruby/Rails中通过has_many :through实现数值递减操作

问题描述

我是Ruby新手,目前在Ruby/Rails中遇到问题:如何通过has_many :through关联表实现数据库数值递减。我的表包括food_product、menu_item和sale,需求是点击menu_item的“售出该商品”按钮后,递减food_product表中的订单数量。

当前代码

FoodProduct 模型

class FoodProduct < ApplicationRecord
  has_many :sales
  has_many :menu_items, :through => :sales
  
  validates :food_, presence: true
  validates :amount_ordered_, presence: true
  validates :amount_to_sell_, presence: true
  validates :amount_of_pans_, presence: true
  validates :date_ordered_, presence: true
  validates :date_order_arrives_, presence: true
  validates :soft_out_date_, presence: true
  validates :hard_out_date_, presence: true
end

MenuItem 模型

class MenuItem < ApplicationRecord
  has_many :sales
  has_many :food_products, :through => :sales

end

Sale 模型

class Sale < ApplicationRecord
  belongs_to :food_product
  belongs_to :menu_item
end

数据库 Schema

create_table "food_products", force: :cascade do |t|
  t.string "food_"
  t.integer "amount_ordered_"
  t.integer "amount_to_sell_"
  t.integer "amount_of_pans_"
  t.integer "orders_per_pan_"
  t.datetime "date_ordered_"
  t.datetime "date_order_arrives_"
  t.datetime "soft_out_date_"
  t.datetime "hard_out_date_"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

create_table "menu_items", force: :cascade do |t|
  t.string "name_"
  t.string "food_item1_"
  t.string "food_item2_"
  t.string "food_item3_"
end

create_table "sales", force: :cascade do |t|
  t.bigint "food_product_id"
  t.bigint "menu_item_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["food_product_id"], name: "index_sales_on_food_product_id"
  t.index ["menu_item_id"], name: "index_sales_on_menu_item_id"
end

解决方案

1. 核心逻辑梳理

点击「售出MenuItem」时,需对该MenuItem关联的所有FoodProduct执行库存递减(若只需操作单个特定FoodProduct,可调整逻辑)。核心是保证创建Sale记录和更新库存的原子性,避免数据不一致。

2. 封装业务方法(推荐)

在MenuItem模型中添加sell!方法,用事务包裹操作,确保要么全部成功,要么全部回滚:

class MenuItem < ApplicationRecord
  has_many :sales
  has_many :food_products, through: :sales

  def sell!
    ActiveRecord::Base.transaction do
      food_products.each do |fp|
        # 提前检查库存是否充足
        if fp.amount_ordered_ <= 0
          raise ActiveRecord::Rollback, "#{fp.food_} 库存不足,无法售出"
        end
        # 创建Sale关联记录
        sales.create!(food_product: fp)
        # 原子性递减库存(Rails内置方法,避免并发冲突)
        fp.decrement!(:amount_ordered_)
      end
    end
    true
  end
end

decrement!会直接在数据库执行amount_ordered_ = amount_ordered_ - 1的原子操作,同时触发模型验证,比手动更新更安全。

3. 控制器处理请求

在MenuItemsController中添加sell动作,接收按钮请求:

class MenuItemsController < ApplicationController
  def sell
    @menu_item = MenuItem.find(params[:id])
    @menu_item.sell!
    redirect_to @menu_item, notice: '商品售出成功'
  rescue ActiveRecord::Rollback => e
    redirect_to @menu_item, alert: e.message
  rescue ActiveRecord::RecordInvalid => e
    redirect_to @menu_item, alert: e.message
  end
end

4. 配置路由

在config/routes.rb中添加成员路由:

resources :menu_items do
  post :sell, on: :member
end

5. 视图添加按钮

在MenuItem的展示页面(如app/views/menu_items/show.html.erb)添加触发按钮:

<%= button_to '售出该商品', sell_menu_item_path(@menu_item), method: :post, class: 'btn btn-primary' %>

关键注意点

  • 并发安全:必须使用原子操作(decrement!/update_all),避免多个请求同时修改导致库存数值错误。
  • 库存校验:在事务中提前检查库存,不足时回滚操作并提示用户。
  • 事务包裹:将创建Sale和更新库存放在同一个事务里,保证数据一致性。
  • 字段名:注意你的模型字段都以_结尾(如amount_ordered_),不要拼写错误。

内容的提问来源于stack exchange,提问作者willpratt78

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 20:27:26