如何在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
相关产品推荐
相关产品推荐

