wch*_*gin 7 postgresql database-design referential-integrity
我有一个表,其中有一列的值为外键,但外键的目标表因行而异。相关表可以仅根据键值来确定,并且存在一小组固定的此类表。
\n我想在此处添加外键约束,以便我的 DBMS 可以确保引用完整性。当然,我不能直接执行此操作,但我有一个建议的解决方案,其中涉及带有传入和传出外键约束的中间“转发表”。我正在寻找评论:
\nGENERATED ALWAYS AS ... STOREDPostgres列的这种使用是否合理或可疑;为了说明该解决方案,请考虑一个存储“用户”和“组”的简单数据库。用户和组均由整数 ID 作为键控,\n并且 ID 的某些位被保留以告知其 ID 类型:
\n-- User and group IDs are both integers, but are in disjoint subsets of the key\n-- space, distinguished by the low 8 bits.\nCREATE DOMAIN userid AS int8 CHECK ((VALUE & 255) = 1);\nCREATE DOMAIN groupid AS int8 CHECK ((VALUE & 255) = 2);\n\nCREATE TABLE users(\n user_id userid PRIMARY KEY,\n name text NOT NULL\n);\nCREATE TABLE groups(\n group_id groupid PRIMARY KEY,\n admin userid NOT NULL REFERENCES users\n);\n\nINSERT INTO users(user_id, name) VALUES (1, \'alice\'), (257, \'bob\');\nINSERT INTO groups(group_id, admin) VALUES (2, 1), (258, 1);\nRun Code Online (Sandbox Code Playgroud)\n现在,用户和组都可以创建发票。无论是由用户还是组创建,发票都具有完全相同的数据,因此我们只需使用一个表来存储创建发票的“参与者”(用户或组)的 ID 以及额外数据:
\n-- Invoices can be created by either users or groups: collectively, "actors".\nCREATE DOMAIN actorid AS int8 CHECK ((VALUE & 255) IN (1, 2));\nCREATE TABLE invoices(\n actor actorid NOT NULL,\n create_time timestamptz NOT NULL,\n amount_cents int NOT NULL\n);\nRun Code Online (Sandbox Code Playgroud)\n现在,从语义上讲,是\n或的invoices.actor外键,具体取决于 的值。没有办法直接为此编写约束。我们可以想象\n定义一个包含所有 actor ID\xe2\x80\x94 的视图usersgroupsactor & 255REFERENCES
CREATE VIEW all_actor_ids AS (\n SELECT user_id AS actor FROM users\n UNION ALL\n SELECT group_id AS actor FROM groups\n);\nRun Code Online (Sandbox Code Playgroud)\n\xe2\x80\x94这样,原则上,actor actorid REFERENCES all_actor_ids,但是\n Postgres 实际上不允许引用foreign\nkeys 中的视图。
为了解决这个问题,我们基本上将其具体all_actor_ids化为一个表,该表本身具有外键约束以确保其自身的完整性:
CREATE TABLE actors(\n actor actorid PRIMARY KEY,\n user_id userid\n REFERENCES users\n GENERATED ALWAYS AS (CASE WHEN (actor & 255) = 1 THEN actor END) STORED,\n group_id groupid\n REFERENCES groups\n GENERATED ALWAYS AS (CASE WHEN (actor & 255) = 2 THEN actor END) STORED,\n CONSTRAINT actors_exactly_one_key\n CHECK (1 = (user_id IS NOT NULL)::int + (group_id IS NOT NULL)::int)\n);\nRun Code Online (Sandbox Code Playgroud)\n现在,invoices.actor可以参考actors:
ALTER TABLE invoices ADD FOREIGN KEY (actor) REFERENCES actors;\nRun Code Online (Sandbox Code Playgroud)\n这个想法是,在代表参与者添加发票之前,\n首先运行INSERT INTO actors(actor) VALUES($1) ON CONFLICT DO NOTHING。\n生成的列负责填充user_idxor\n group_id,这些列上的外键约束确保\n底层实体实际存在,如果之前已经使用过该 actor,则冲突处理程序会使该操作成为无操作。
例如,通过上述定义,这些插入可以工作:
\n-- All users and groups can be populated as actors.\nINSERT INTO actors(actor)\n SELECT user_id FROM users UNION ALL SELECT group_id FROM groups\n ON CONFLICT DO NOTHING;\n\n-- Invoices can be created for either actors or groups.\nINSERT INTO invoices(actor, create_time, amount_cents)\n VALUES (1, now(), 100), (258, now(), 200);\nRun Code Online (Sandbox Code Playgroud)\n请注意,actors数据实际上不需要成为JOIN读取路径的一部分。它的存在只是为了将外键约束引入\n提交。
在我看来,这个解决方案应该正确确保引用\n完整性:特别是,如果不级联删除该用户或组创建的任何发票,则无法删除用户或组。\n但我有一些问题:
\n我是否遗漏了一些边缘情况,在这种情况下,该解决方案实际上并不能确保引用完整性?
\n假设发票现在也可以由第三种类型的实体创建:比如说robots。我认为我可以更改actorid域\n以合并s,然后像其他列一样robotid添加一个新列\n并更新约束。\n是否存在我应该警惕的潜在问题?actors.robot_idactors_exactly_one_key
我之前没有使用过 PostgresGENERATED ALWAYS AS ... STORED列,而且我有点担心默认表达式在事后根本无法更改。这看起来是否适合\n使用生成的列,或者用CHECK确保相同\n值但要求用户提供它们的约束来替换\n生成的列会更好吗?
是个INSERT INTO actors(actor) ... ON CONFLICT DO NOTHING可能引入并发问题?(或者,是否还有其他我错过的明显性能问题?)
任何其他反馈或评论也受到热烈欢迎。
\n我正在使用 Postgres 12,但如果这里的最佳解决方案需要升级到 Postgres 14,我对此持开放态度。
\n你这不是把自己想得太复杂了吗?
更简单:
为每个可能的 FK 创建 1 列,然后添加 CHECK 约束以确保只有 1 列不为 NULL,其余列保持为 NULL(如果您只需要 1 个 FK,您可能需要更多)。
也简单得多:
将 FK 添加到其他表,其他表都将具有到同一个主表的FK ,这样数据库就不会限制可能有多少关系,但这可能很容易通过应用程序逻辑来完成,并且带来好处一个看起来非常健壮的数据库模式。(如果您对主表可能有多少关系的要求发生变化,您甚至不必运行迁移)。
就我个人而言,如果可能的话,我更愿意避免使用“实现”这个词。