在有多个模型关联操作的情况下,使用Callbacks保证数据的一致性

背景

通过callback关联的2个model,rails会自动把所有的数据库操作语句封装为一个事务。如果整个事务执行过程中有异常抛出,事务将自动回滚,从而保证了关联模型的数据一致性。

举例

在电商系统中,有2个模型,Order和OrderItem。其中Order模型用来存储订单信息,OrderItem存储订单项。这2个表通过一个生成的orderID来进行关联。 数据库schema如下:

create_table "order_items", force: :cascade do |t|
  t.string   "orderID"
  t.integer  "product_id"
  t.decimal  "price",      precision: 10, scale: 2
  t.decimal  "amount",     precision: 10, scale: 2
  t.datetime "created_at",                          null: false
  t.datetime "updated_at",                          null: false
  t.integer  "user_id"
  t.index ["product_id"], name: "index_order_items_on_product_id", using: :btree
end

create_table "orders", force: :cascade do |t|
  t.integer  "user_id"
  t.string   "orderID"
  t.text     "message"
  t.datetime "created_at",                             null: false
  t.datetime "updated_at",                             null: false
  t.string   "fixed_address"
  t.decimal  "total_price",   precision: 10, scale: 2
  t.index ["user_id"], name: "index_orders_on_user_id", using: :btree
end

业务逻辑是:

  1. 如果创建了订单,则需要将购物车中具体购买的物品添加到order_items表中。
  2. 如果删除订单,则需要同时删除在order_items表中具有和orderID相同的项目。

使用Callback实现关联操作如下:

class Order < ApplicationRecord
  belongs_to :user
  belongs_to :address

  # 新建order后新建order_items
  after_create :create_order_items

  # 如果删除订单,则需要同时删除在order_items表中具有和orderID相同的项目。
  after_destroy :destroy_all_same_orderID

  protected
    def create_order_items
      current_user = self.user
      current_user.carts.each do |cart|
        # 这里使用save!,而不是save,因为save只会返回true或false,而不会抛出异常。
        OrderItem.new(user_id: current_user.id, orderID: self.orderID, product_id: cart.product_id, price: cart.product.price, amount: cart.amount).save!
      end
      # raise "crash for test"
      Cart.where(user_id: current_user.id).destroy_all
    end

    def destroy_all_same_orderID
      # raise "error"

      OrderItem.where(orderID: self.orderID).destroy_all
    end
end

结论

这样在orders_controller.rb中的create操作,只进行order新建相关的操作,没有任何order_items相关的操作。如果不使用callback,在controller中操作两个模型的话,代码会不整洁,最主要是数据一直性无法保证。另外,好的做法是将callback方法放在private或protected中,以避免对外公开。

注意

这种做法可能导致callback hell(回调地狱),不要在数据层写入过多的业务逻辑。