触发以确保合计总和小于不同表中的值

mis*_*nry 6 sql postgresql triggers database-design sql-insert

我正在处理的问题领域是电子商务的退货管理。

我正在使用 Postgres (11.9) 并具有以下表格(我已从每个表中删除了一些与问题无关的字段):

CREATE TABLE "order" (
    id BIGSERIAL PRIMARY KEY,
    platform text NOT NULL,
    platform_order_id text NOT NULL,
    CONSTRAINT platform_order_id_unique UNIQUE (platform, platform_order_id)
);

CREATE TABLE order_item (
    id BIGSERIAL PRIMARY KEY,
    order_id int8 NOT NULL,
    platform_item_id text NOT NULL,
    quantity integer,
    CONSTRAINT FK_order_item_order_id FOREIGN KEY (order_id) REFERENCES "order",
    CONSTRAINT platform_item_id_unique UNIQUE (order_id, platform_item_id)
);

CREATE TABLE return (
    id BIGSERIAL PRIMARY KEY,
    order_id int8 NOT NULL,
    CONSTRAINT FK_return_order_id FOREIGN KEY (order_id) REFERENCES "order"
);

CREATE TABLE return_item (
    return_id int8 NOT NULL,
    order_item_id int8 NOT NULL,
    quantity integer NOT NULL,
    CONSTRAINT FK_return_item_return_id FOREIGN KEY (return_id) REFERENCES return,
    CONSTRAINT FK_return_item_item_id FOREIGN KEY (order_item_id) REFERENCES order_item
);
Run Code Online (Sandbox Code Playgroud)

为了简要解释该域,我从电子商务平台提取订单并将其存储在我的数据库中。订单由一个或多个带有quantity > 1. 当用户希望退货时,他们最多可以退回每次退货的数量。

更具体地说,如果我在一个订单中购买两件黑色小T恤,您会order在数据库中找到一个order_item数量为 的单件2。我将能够创建两个单独的回报,每个回报都有一个return_item引用相同order_item_id但数量为 1 的回报。

order_itemreturn_item插入到不同的事务中,并且我不会阻止多个事务同时更新其中任何一个事务。

如何确保特定的每个for quantityall的总和值不超过相应for with said中存储的数量?return_itemorder_item_idorder_itemid

用更简单的英语来说,当原始订单中第三件商品的数量为 2(如我所描述的示例中所示)时,如何防止该商品被退回?

在大多数情况下,编写应用程序检查来捕获此问题很容易,并且WHERE向我的return_item插入添加业务规则检查子句也不难,但是这些解决方案都没有为我提供唯一性约束所提供的一致性保证。我将如何编写一个触发器以在此处插入时出错?或者有比触发器更好的方法吗?

Erw*_*ter 2

您专门要求触发解决方案。根据记录,您也可以使用纯 SQL 实现相同的目的,只要您可以确保所有客户端都使用必要的语句。相关示例:

触发解决方案

您提到并发写入访问是可能的。这使得事情变得更加复杂。例如,两个事务可能会尝试同时从同一order_item事务返回一个项目。两者都检查并发现可以返回另外一项并执行此操作,从而超出了order_item.quantity1 的数量。经典并发警告。

为了防御它,您可以使用SERIALIZABLE事务隔离。但这要昂贵得多,并且所有可能写入相关表的事务都必须坚持它。

或者,在默认隔离级别中删除策略行锁READ COMMITTED。这是一个基本的实现:

触发功能:

CREATE FUNCTION trg_return_item_insup_bef()
  RETURNS trigger
  LANGUAGE plpgsql AS
$func$
DECLARE
   _ordered_items int;
   _remaining_items int;
BEGIN
   SELECT quantity
   FROM   order_item
   WHERE  id = NEW.order_item_id
   FOR    NO KEY UPDATE                -- lock the parent row first ... (!!!)
   INTO   _ordered_items;              --  ... while fetching quantity

   SELECT _ordered_items - COALESCE(sum(quantity), 0)
   FROM   return_item
   WHERE  order_item_id = NEW.order_item_id
   INTO   _remaining_items;

   IF NEW.quantity > _remaining_items THEN
      RAISE EXCEPTION 'Tried to return % items, but only % of % are left.'
                     , NEW.quantity, _remaining_items, _ordered_items;
   END IF;
   
   RETURN NEW;
END
$func$;
Run Code Online (Sandbox Code Playgroud)

扳机:

CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE ON return_item
FOR EACH ROW
EXECUTE PROCEDURE trg_return_item_insup_bef();
Run Code Online (Sandbox Code Playgroud)

db<>在这里摆弄

任何返回项目的尝试都会order_item首先锁定父行。竞争事务必须等到这一事务被提交,然后才能看到新提交的行。这消除了竞争条件。FOR NO KEY UPDATE是正确的锁定强度。既不能太弱也不能太强。

写入order_item也会干扰项目总数。但它们也会(隐式地)取出写锁,并被迫以相同的方式排队。但如果以后可以更新order_item.quantity,您将必须在触发器中添加类似的检查(以防其降低)。

我将基本信息添加到超出数量时引发的错误消息中。您可以在那里放置更多或更少的信息。

示例设置可以优化。“order”是保留字。该表return在示例中毫无用处,也是如此return_item.return_id。PK 中缺失return_itemorder_item.quantity应该NOT NULL CHECK (quantity > 0)COALESCE在正确的实现中,触发函数是多余的。但这些都是次要的注释。