我在Postgres 9.3数据库中有以下场景:表B和C参考表A;表C有一个引用表B的附加可选字段。我想确保表C的每一行c引用表B,c.b.a = c.a。
在这种情况下,是否有更好的方法来强制执行数据完整性?
答案 0 :(得分:3)
有一个非常简单的防弹解决方案。适用于 Postgres 9.3 - 当提出原始问题时。适用于当前的 Postgres 13 - 添加赏金中的问题时:
<块引用>想知道是否可以在没有数据库触发器的情况下实现这一点
FOREIGN KEY
constraints 可以跨越多列。只需在从表 C 到表 B 的 FK 约束中包含表 A 的 ID。这会强制 B 和 C 中的链接行始终指向 A 中的同一行。如:
CREATE TABLE a (
a_id int PRIMARY KEY
);
CREATE TABLE b (
b_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, UNIQUE (a_id, b_id) -- redundant, but required for FK
);
CREATE TABLE c (
c_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, b_id int
, CONSTRAINT fk_simple_and_safe_solution
FOREIGN KEY (a_id, b_id) REFERENCES b(a_id, b_id) -- THIS !
);
最小样本数据:
INSERT INTO a(a_id) VALUES
(1)
, (2);
INSERT INTO b(b_id, a_id) VALUES
(1, 1)
, (2, 2);
INSERT INTO c(c_id, a_id, b_id) VALUES
(1, 1, NULL) -- allowed
, (2, 2, 2); -- allowed
按要求禁止:
INSERT INTO c(c_id, a_id, b_id) VALUES (3,2,1);
<块引用>
ERROR: insert or update on table "c" violates foreign key constraint "fk_simple_and_safe_solution"
DETAIL: Key (a_id, b_id)=(2, 1) is not present in table "b".
db<>fiddle here
FK 约束的默认 MATCH SIMPLE
行为如下所示 (quoting the manual):
MATCH SIMPLE
允许任何外键列为空;如果它们中的任何一个为空,则该行不需要在引用的表中具有匹配项。
所以 c(b_id)
中的 NULL 值仍然是允许的(根据要求:“可选字段”)。对于这种特殊情况,FK 约束被“禁用”。
我们需要 UNIQUE
上的逻辑冗余 b(a_id, b_id)
约束以允许 FK 引用它。但是通过在 (a_id, b_id)
而不是 (b_id, a_id)
,它本身也很有用,在 b(a_id)
上提供有用的索引以支持其他 FK 约束等.见:
(c(a_id)
上的附加索引通常很有用。)
进一步阅读:
答案 1 :(得分:2)
Would like information on if this is possible to achieve without database triggers
是的,这是可能的。该机制称为ASSERTION,它在 SQL-92 标准中定义(尽管它没有被任何主要的 RDBMS 实现)。
简而言之,它允许创建多行约束或多表检查约束。
对于 PostgreSQL,它可以通过使用带有 WITH CHECK OPTION
的视图并对视图而不是基表执行操作来模拟。
此选项控制可自动更新的视图的行为。指定此选项时,将检查视图上的 INSERT 和 UPDATE 命令以确保新行满足视图定义条件(即检查新行以确保它们通过视图可见) .如果不是,更新将被拒绝。
示例:
CREATE TABLE a(id INT PRIMARY KEY, cola VARCHAR(10));
CREATE TABLE b(id INT PRIMARY KEY, colb VARCHAR(10), a_id INT REFERENCES a(id) NOT NULL);
CREATE TABLE c(id INT PRIMARY KEY, colc VARCHAR(10),
a_id INT REFERENCES a(id) NOT NULL,
b_id INT REFERENCES b(id));
样本插入:
INSERT INTO a(id, cola) VALUES (1, 'A');
INSERT INTO a(id, cola) VALUES (2, 'A2');
INSERT INTO b(id, colb, a_id) VALUES (12, 'B', 1);
INSERT INTO c(id, colc, a_id) VALUES (15, 'C', 2);
违反条件(在两个表上用B不同的a_id连接C)
UPDATE c SET b_id = 12 WHERE id = 15;;
-- no issues whatsover
创建视图:
CREATE VIEW view_c
AS
SELECT *
FROM c
WHERE NOT EXISTS(SELECT 1
FROM b
WHERE c.b_id = b.id
AND c.a_id != b.a_id) -- here is the clue, we want a_id to be the same
WITH CHECK OPTION ;
尝试第二次更新(错误):
UPDATE view_c SET b_id = 12 WHERE id = 15;
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (15, C, 2, 12).
尝试使用不正确的数据(也有错误)插入全新的插入内容
INSERT INTO b(id, colb, a_id) VALUES (20, 'B2', 2);
INSERT INTO view_c(id, colc, a_id, b_id) VALUES (30, 'C2', 1, 20);
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (30, C2, 1, 20)
答案 2 :(得分:1)
我最终创建了一个触发器,如下所示:
create function "check C.A = C.B.A"()
returns trigger
as $$
begin
if NEW.b is not null then
if NEW.a != (select a from B where id = NEW.b) then
raise exception 'a != b.a';
end if;
end if;
return NEW;
end;
$$
language plpgsql;
create trigger "ensure C.A = C.B.A"
before insert or update on C
for each row
execute procedure "check C.A = C.B.A"();