postgres:涉及多个表的外键约束

时间:2014-09-26 17:33:53

标签: postgresql schema constraints

我在Postgres 9.3数据库中有以下场景:表B和C参考表A;表C有一个引用表B的附加可选字段。我想确保表C的每一行c引用表B,c.b.a = c.a。

  • 我可以重构表C,这样如果指定了c.b,则c.a为null但这会使得连接表A和C的查询变得笨拙。
  • 我也许能够使表B的主键包括它对表A的引用,然后使表C的表B的外键包括表C对表A的引用,但我认为这种调整太难以证明其好处
  • 我认为这可以通过在表C上插入/更新之前运行的触发器来完成,并拒绝违反指定约束的操作。

在这种情况下,是否有更好的方法来强制执行数据完整性?

3 个答案:

答案 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 的视图并对视图而不是基表执行操作来模拟。

<块引用>

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)

db<>fiddle demo

答案 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"();