触发以确保总和小于不同 table 中的值
Trigger to ensure aggregate sum less than a value in a different table
我工作的问题领域是return电子商务管理。
我正在使用 Postgres (11.9) 并具有以下 table(我从每个 table 中删除了一些与问题无关的字段):
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
);
为了简要说明域名,我从电子商务平台提取订单并将其存储在我的数据库中。一个订单由一个或多个具有 quantity > 1
的不同项目组成。当用户希望 return 一件商品时,他们可以 return 达到每个 return.
的数量
更具体地说,如果我在一个订单中购买两件黑色小 T 恤,您会在数据库中找到一件 order
,其中一件 order_item
的数量为 2
。我将能够创建两个单独的 return,每个 return 有一个 return_item
引用相同的 order_item_id
但数量为 1.
order_item
和 return_item
被插入到不同的事务中,我不会阻止多个事务同时更新其中任何一个。
如何确保具有特定 order_item_id
的所有 return_item
的每个 quantity
的总值不超过存储在相应 order_item
中的数量用说 id
?
用更简单的英语来说,当原始订单中该商品的数量为 2 时,我如何防止 return编辑该商品,如我所描述的示例?
在大多数情况下,编写应用程序检查来捕获此问题很容易,并且向我的 return_item
插入添加业务规则检查 WHERE
子句也不难,但这些解决方案都不是给我唯一性约束所做的一致性保证。我将如何编写触发器以在此处插入时出错?或者有比触发器更好的方法吗?
我能想到的唯一解决方案是反规范化。
将 integer
列 total_returns
添加到 order_item
,每当添加或删除行或 quantity
更改时,都会由 return_item
上的触发器修改.
然后您可以对 order_item
进行简单的检查约束,以确保您的不变量成立。
一些示例代码:
BEGIN;
/* for consistency */
ALTER TABLE order_item
ALTER quantity SET NOT NULL
ALTER quantity SET DEFAULT 0;
ALTER TABLE order_item
ADD total_returns bigint DEFAULT 0 NOT NULL;
ALTER TABLE order_item
ADD CONSTRAINT not_too_many_returns
CHECK (total_returns <= quantity);
/* trigger function */
CREATE FUNCTION requrn_order_trig() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
UPDATE order_item
SET total_returns = total_returns + NEW.quantity;
WHERE id = NEW.order_item_id;
END IF;
IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
UPDATE order_item
SET total_returns = total_returns - OLD.quantity;
WHERE id = OLD.order_item_id;
END IF;
RETURN NULL;
END;$$;
CREATE TRIGGER requrn_order_trig
AFTER INSERT OR UPDATE OR DELETE ON return_item
FOR EACH ROW EXECUTE PROCEDURE requrn_order_trig();
UPDATE order_item AS oi
SET total_returns = (SELECT sum(quantity)
FROM return_item AS r
WHERE r.order_item_id = oi.id);
COMMIT;
我不建议存储这些派生信息,因为维护起来很乏味。
相反,您可以在 DML 查询中实现逻辑。考虑以下查询来注册一个新的 returned 项目:
insert into return_item (order_item_id, quantity)
select v.*
from (values (, )) as v(order_item_id, quantity)
inner join order_items oi on oi.id = v.order_item_id
where oi.quantity >= v.quantity + (
select coalesce(sum(ri.quantity), 0) from return_item ri where ri.order_item_id = v.order_item_id
)
输入值以 </code> 和 <code>
的形式给出。查询在 order_items
中引入相应的行,并检查该项目的总 returned 数量是否大于订购数量。
整个逻辑在单个查询中实现,因此如果您有多个并发进程,则不存在竞争条件的风险。
在您的应用程序中,您可以检查查询是否影响了任何行。如果没有,那么您就知道 return 被拒绝了。
如果要定期使用,可以将查询放在存储过程中。
你专门要求触发方案。作为记录,您也可以使用普通 SQL 实现相同的效果,只要您可以确保所有客户端都使用必要的语句即可。相关范例:
- Delete parent if it's not referenced by any other child
触发解决方案
您提到并发写访问是可能的。这使它变得更加复杂。例如,两个事务可能会同时尝试 return 来自同一个 order_item
的项目。两者都检查并发现可以 returned 并这样做,从而超过 order_item.quantity
的数量 1。经典并发警告。
为了防御它,您可以使用 SERIALIZABLE
事务隔离。但这相当昂贵,并且所有可能写入涉及的 tables 的事务都必须坚持它。
或者,取消战略 row locks in default 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$;
触发器:
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE ON return_item
FOR EACH ROW
EXECUTE PROCEDURE trg_return_item_insup_bef();
db<>fiddle here
任何对 return 项的尝试都会首先锁定 order_item
中的父行。竞争事务必须等到这个事务被提交——然后才会看到新提交的行。这消除了竞争条件。 FOR NO KEY UPDATE
是正确的锁强度。既不弱也不强。
写入 order_item
也会影响项目总数。但是那些也(隐含地)取出写锁并被迫以相同的方式排队。但是,如果以后可以对 order_item.quantity
进行更新,则您必须在那里的触发器中添加类似的检查(以防它被降低)。
我在超出数量时出现的错误消息中添加了基本信息。您可能会在此处添加或多或少的信息。
可以优化示例设置。 “order”是一个保留字。 table return
在示例中没有用,return_item.return_id
也是如此。 return_item
中缺少 PK。 order_item.quantity
应该是 NOT NULL CHECK (quantity > 0)
。 COALESCE
在触发函数中的正确实现是多余的。但这些都是次要笔记。
我工作的问题领域是return电子商务管理。
我正在使用 Postgres (11.9) 并具有以下 table(我从每个 table 中删除了一些与问题无关的字段):
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
);
为了简要说明域名,我从电子商务平台提取订单并将其存储在我的数据库中。一个订单由一个或多个具有 quantity > 1
的不同项目组成。当用户希望 return 一件商品时,他们可以 return 达到每个 return.
更具体地说,如果我在一个订单中购买两件黑色小 T 恤,您会在数据库中找到一件 order
,其中一件 order_item
的数量为 2
。我将能够创建两个单独的 return,每个 return 有一个 return_item
引用相同的 order_item_id
但数量为 1.
order_item
和 return_item
被插入到不同的事务中,我不会阻止多个事务同时更新其中任何一个。
如何确保具有特定 order_item_id
的所有 return_item
的每个 quantity
的总值不超过存储在相应 order_item
中的数量用说 id
?
用更简单的英语来说,当原始订单中该商品的数量为 2 时,我如何防止 return编辑该商品,如我所描述的示例?
在大多数情况下,编写应用程序检查来捕获此问题很容易,并且向我的 return_item
插入添加业务规则检查 WHERE
子句也不难,但这些解决方案都不是给我唯一性约束所做的一致性保证。我将如何编写触发器以在此处插入时出错?或者有比触发器更好的方法吗?
我能想到的唯一解决方案是反规范化。
将 integer
列 total_returns
添加到 order_item
,每当添加或删除行或 quantity
更改时,都会由 return_item
上的触发器修改.
然后您可以对 order_item
进行简单的检查约束,以确保您的不变量成立。
一些示例代码:
BEGIN;
/* for consistency */
ALTER TABLE order_item
ALTER quantity SET NOT NULL
ALTER quantity SET DEFAULT 0;
ALTER TABLE order_item
ADD total_returns bigint DEFAULT 0 NOT NULL;
ALTER TABLE order_item
ADD CONSTRAINT not_too_many_returns
CHECK (total_returns <= quantity);
/* trigger function */
CREATE FUNCTION requrn_order_trig() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
UPDATE order_item
SET total_returns = total_returns + NEW.quantity;
WHERE id = NEW.order_item_id;
END IF;
IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
UPDATE order_item
SET total_returns = total_returns - OLD.quantity;
WHERE id = OLD.order_item_id;
END IF;
RETURN NULL;
END;$$;
CREATE TRIGGER requrn_order_trig
AFTER INSERT OR UPDATE OR DELETE ON return_item
FOR EACH ROW EXECUTE PROCEDURE requrn_order_trig();
UPDATE order_item AS oi
SET total_returns = (SELECT sum(quantity)
FROM return_item AS r
WHERE r.order_item_id = oi.id);
COMMIT;
我不建议存储这些派生信息,因为维护起来很乏味。
相反,您可以在 DML 查询中实现逻辑。考虑以下查询来注册一个新的 returned 项目:
insert into return_item (order_item_id, quantity)
select v.*
from (values (, )) as v(order_item_id, quantity)
inner join order_items oi on oi.id = v.order_item_id
where oi.quantity >= v.quantity + (
select coalesce(sum(ri.quantity), 0) from return_item ri where ri.order_item_id = v.order_item_id
)
输入值以 </code> 和 <code>
的形式给出。查询在 order_items
中引入相应的行,并检查该项目的总 returned 数量是否大于订购数量。
整个逻辑在单个查询中实现,因此如果您有多个并发进程,则不存在竞争条件的风险。
在您的应用程序中,您可以检查查询是否影响了任何行。如果没有,那么您就知道 return 被拒绝了。
如果要定期使用,可以将查询放在存储过程中。
你专门要求触发方案。作为记录,您也可以使用普通 SQL 实现相同的效果,只要您可以确保所有客户端都使用必要的语句即可。相关范例:
- Delete parent if it's not referenced by any other child
触发解决方案
您提到并发写访问是可能的。这使它变得更加复杂。例如,两个事务可能会同时尝试 return 来自同一个 order_item
的项目。两者都检查并发现可以 returned 并这样做,从而超过 order_item.quantity
的数量 1。经典并发警告。
为了防御它,您可以使用 SERIALIZABLE
事务隔离。但这相当昂贵,并且所有可能写入涉及的 tables 的事务都必须坚持它。
或者,取消战略 row locks in default 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$;
触发器:
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE ON return_item
FOR EACH ROW
EXECUTE PROCEDURE trg_return_item_insup_bef();
db<>fiddle here
任何对 return 项的尝试都会首先锁定 order_item
中的父行。竞争事务必须等到这个事务被提交——然后才会看到新提交的行。这消除了竞争条件。 FOR NO KEY UPDATE
是正确的锁强度。既不弱也不强。
写入 order_item
也会影响项目总数。但是那些也(隐含地)取出写锁并被迫以相同的方式排队。但是,如果以后可以对 order_item.quantity
进行更新,则您必须在那里的触发器中添加类似的检查(以防它被降低)。
我在超出数量时出现的错误消息中添加了基本信息。您可能会在此处添加或多或少的信息。
可以优化示例设置。 “order”是一个保留字。 table return
在示例中没有用,return_item.return_id
也是如此。 return_item
中缺少 PK。 order_item.quantity
应该是 NOT NULL CHECK (quantity > 0)
。 COALESCE
在触发函数中的正确实现是多余的。但这些都是次要笔记。