Mar*_*ers 5 postgresql trigger physical-design unique-constraint
我们定义了一系列配置,其中,在 RESTful API 的驱动下,最终用户可以构建新的修订版本。配置的某些组件可以有多个值;修订涉及具有一对多关系的多个表。
因为配置被运往别处,修订被标记为已部署,并且变得不可变。如果用户想对配置进行更改,他们必须创建一个新修订(可以从现有修订中克隆)。每个配置的一个 修订版可以标记为“当前”;这允许用户随意在过去的修订之间切换,或者通过不选择任何修订来完全禁用配置。当前版本已部署,当将不同版本标记为“当前”时,您将替换已部署的配置。
我们已经准备好了一切来强制部署修订的不变性;当您第一次使用修订作为当前修订时,该deployed
列会自动转换为TRUE
,并且所有进一步的INSERT
,UPDATE
和DELETE
与修订相关表中部署的修订 ID 匹配的行的操作都将被阻止。
但是,用于公共名称表中的name
列的任何值在所有当前配置的所有“当前”修订中都必须是唯一的。我正在尝试找出执行此操作的最佳策略。
如果这是从配置到公共名称的简单一对多关系,则可以通过对name
列使用唯一约束来解决。相反,这是一种一对多模式,revision
充当桥接表,并将current_revision_id
一对多对多关系“折叠”为从配置到虚拟的一对多关系公共名称。
这是一组简化的表格,用于说明我们的情况:
Run Code Online (Sandbox Code Playgroud)-- Configurations CREATE TABLE config ( id INT PRIMARY KEY, name VARCHAR(100), current_revision_id INT ); -- Have multiple revisions CREATE TABLE revision ( id INT PRIMARY KEY, config_id INT NOT NULL REFERENCES config(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, description VARCHAR, foo INT NOT NULL, bar BOOLEAN NOT NULL, deployed BOOLEAN NOT NULL DEFAULT FALSE ); -- A configuration has one _current_ revision ALTER TABLE config ADD CONSTRAINT current_revision_id_fk FOREIGN KEY (current_revision_id) REFERENCES revision(id); -- Revisions are automatically numbered in a view CREATE VIEW numbered_revision AS ( SELECT *, row_number() OVER ( PARTITION BY config_id ORDER BY created_at, id ) AS number FROM revision ); -- Configurations have multiple 'public names' CREATE TABLE public_name ( id INT PRIMARY KEY, revision_id INT NOT NULL REFERENCES revision(id), name VARCHAR(100), UNIQUE (revision_id, name) );
该视图仅用于为每个配置提供具有无间隙编号的修订(修订永远不会被删除)。
作为ERD图:
一些示例数据来说明设置:
Run Code Online (Sandbox Code Playgroud)INSERT INTO config (id, name) VALUES (17, 'config_foo'), (42, 'config_bar'); INSERT INTO revision (id, config_id, created_at, description, foo, bar) VALUES (11, 17, '2021-05-29 09:07:18', 'Foo configuration, first draft', 81, TRUE), (19, 17, '2021-05-29 10:42:17', 'Foo configuration, second draft', 73, TRUE), (23, 42, '2021-05-29 09:36:52', 'Bar configuration, first draft', 118, FALSE); INSERT INTO public_name (id, revision_id, name) VALUES -- public names for foo configuration, first draft (83, 11, 'some.name'), (84, 11, 'other.name'), -- public names for foo configuration, second draft (85, 19, 'revised.name'), (86, 19, 'other.name'), (87, 19, 'third.name'), -- public names for bar configuration, first draft; -- some of the names here are the same used by foo configurations (88, 23, 'some.name'), (89, 23, 'unique.name'), (90, 23, 'other.name'); -- Foo configuration has a current, published revision: UPDATE config SET current_revision_id = 19 WHERE id = 17; UPDATE revision SET deployed = TRUE WHERE id in (11, 19);
这是显示示例数据集的查询:
Run Code Online (Sandbox Code Playgroud)SELECT c.name AS config, rev.number AS revision, rev.deployed, CASE WHEN c.current_revision_id = rev.id THEN 'ACTIVE' ELSE '' END AS status, string_agg(p.name, ', ' ORDER BY p.name) AS names FROM config c JOIN numbered_revision AS rev ON c.id = rev.config_id JOIN public_name p ON p.revision_id = rev.id GROUP BY c.id, rev.id, rev.number, rev.deployed ORDER BY c.id, rev.number;
配置 修订 部署 地位 名字 config_foo 1 吨 其他.name, some.name config_foo 2 吨 积极的 other.name , 修订.name, 第三名 配置栏 1 F other.name , some.name, unique.name
db<>在这里摆弄
在上面的输出表中,第二行表示“当前”修订,已公开部署),并且该行已被授予对names
列中公共名称的独占访问权限。
第三行代表具有草稿修订的配置。任何将其设置为当前for 的尝试都config_bar
应该失败,因为该名称other.name
已用于config_foo
,修订版 2。如果将来config_foo
要创建不包含 的新修订版,则other.name
只有这样才能使config_bar
修订版 1 成为最新版。
我们确实预先验证了这个约束;当不满足前提条件时,API 会运行一些检查并阻止将配置标记为当前配置。public_name
表中的名称也被限制为每个修订版是唯一的 ( UNIQUE (revision_id, name)
)。这些都不能防止竞争条件,它们只是降低了竞争条件发生的速度。
我希望 CONSTRAINT TRIGGER on config
,UPDATE
在current_revision_id
列的s上触发,足以强制执行此约束:
CREATE OR REPLACE FUNCTION unique_current_names() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
IF EXISTS (
SELECT 1
FROM public_name p
WHERE
p.revision_id = NEW.current_revision_id
AND p.name IN (
SELECT pp.name
FROM config AS pc
JOIN public_name pp ON pp.revision_id = pc.current_revision_id
AND pc.id != OLD.id
)
) THEN
RAISE EXCEPTION 'Public name is already published';
END IF;
RETURN NEW;
END;$$;
DROP TRIGGER IF EXISTS unique_current_names_trig ON config;
CREATE CONSTRAINT TRIGGER unique_current_names_trig
AFTER UPDATE OF current_revision_id ON config
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE unique_current_names();
Run Code Online (Sandbox Code Playgroud)
(请注意,之间的关系config
,并public_name
在,在一般情况下,许多一对多连接,但对于更具体的current_revision_id
情况下,它是一个一对多的连接,并且可以使用config.current_version_id = public_name.version_id
直接列表中的名称)。
我担心的是,即使这个触发器在事务的最后触发,仍然存在竞争条件的可能性,其中另一个连接也试图使修订版本与公共名称冲突。
OTOH,因为所有的更新和插入是基于REST的API操作的结果,绝不是包括多个操作(升级的交易public_name
,并设置current_revision_id
)。这足以防止这里的竞争条件,还是我错过了一些极端情况?
另一种选择可能是将当前修订的公共名称复制到单独的“已发布名称”表中(带有触发器;删除所有旧名称,插入所有新名称),在那里的名称列上有一个 UNIQUE 约束。这会比约束触发器更好吗?
请注意,我们不能使用名称空间或其他名称(公共互联网上的主机名)来使它们唯一。一旦部署,名称必须完全独立。
我们知道该设计允许配置引用revision_id
属于不同配置的电流。这是我们在应用程序级别明确防范的一种可能性,但触发器也可以处理这种情况。
正如Laurenz Albe 所证实的,使用约束触发器不会阻止竞争条件,除非我们将事务隔离级别SERIALIZABLE
切换到。这会使我们的应用程序逻辑变得复杂,因为我们必须重试提交。
现在,我们不再使用约束触发器,而是使用ON INSERT OR UPDATE
触发器将已发布的公共名称复制到新表中。然后,对该表强制执行唯一约束,因此不会遇到相同的事务隔离问题。如果两个事务尝试将具有冲突名称的修订版提升为公开,则其中一个事务将因唯一约束而失败。
这是published_public_name
表和触发器config
的样子:
CREATE TABLE published_public_name (
id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
config_id INT NOT NULL REFERENCES config(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL UNIQUE
);
CREATE OR REPLACE FUNCTION copy_published_public_names()
RETURNS TRIGGER
AS $BODY$
BEGIN
-- OLD / NEW is a row in the config table
-- Trigger is called for both updates and inserts
IF TG_OP = 'UPDATE' THEN
IF OLD.current_revision_id IS NOT DISTINCT FROM NEW.current_revision_id THEN
-- Nothing changed
RETURN NEW;
END IF;
IF OLD.current_revision_id IS NOT NULL THEN
DELETE FROM published_public_name
WHERE id IN (
SELECT ppn.id
FROM published_public_name ppn
WHERE ppn.config_id = OLD.id
ORDER BY ppn.name
);
END IF;
END IF;
IF NEW.current_revision_id IS NOT NULL THEN
INSERT INTO published_public_name(config_id, name)
SELECT NEW.id, pn.name
FROM public_name pn
WHERE pn.revision_id = NEW.current_revision_id
ORDER BY pn.name;
END IF;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER copy_published_public_names_trigger
BEFORE INSERT OR UPDATE OF current_revision_id ON config
FOR EACH ROW
EXECUTE FUNCTION copy_published_public_names();
Run Code Online (Sandbox Code Playgroud)
这处理INSERT
和UPDATE
。删除是通过外键处理ON DELETE CASCADE
的published_public_name.config_id
。
我在触发器的and语句中包含了一个ORDER BY
子句,以避免在涉及多个名称时出现潜在的死锁。我不能 100% 确定是否需要订购,但如果不需要也没有什么坏处。DELETE
INSERT
DELETE
当尝试发布具有已被另一个配置使用的名称的修订时,该语句失败并显示:
ERROR: duplicate key value violates unique constraint "published_public_name_name_key"
DETAIL: Key (name)=(other.name) already exists.
CONTEXT: SQL statement "INSERT INTO published_public_name(config_id, name)
SELECT NEW.id, pn.name
FROM public_name pn
WHERE pn.revision_id = NEW.current_revision_id"
PL/pgSQL function copy_published_public_names() line 18 at SQL statement
Run Code Online (Sandbox Code Playgroud)
请参阅我的新db<>fiddle 修订版。
归档时间: |
|
查看次数: |
155 次 |
最近记录: |