关于postgresql:涉及多个表的外键约束

Foreign key constraints involving multiple tables

我在Postgres 9.3数据库中具有以下情形:

  • 表B和C引用表A。
  • 表C具有引用表B的可选字段。

我想确保对于引用表B的表C的每一行,c.b.a =c.a。也就是说,如果C引用了B,则两行都应指向表A中的同一行。

  • 我可以重构表C,以便如果指定c.b,则c.a为空,但这会使连接表A和C的查询尴尬。
  • 我也许还可以使表B的主键包含对表A的引用,然后使表C的对表B的外键包含表C的对表A的引用,但是我认为此调整太尴尬了,不足以证明这样做的好处。
  • 我认为可以通过在表C上执行插入/更新操作之前运行触发器并拒绝违反指定约束的操作来完成此操作。

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


有一个非常简单的防弹解决方案。在询问原始问题时适用于Postgres 9.3。适用于当前的Postgres 13-当添加赏金中的问题时:

Would like information on if this is possible to achieve without database triggers

FOREIGN KEY约束可以跨越多列。只需在从表C到表B的FK约束中包括表A的ID。这将强制B和C中的链接行始终指向A中的同一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 !
);

最小样本数据:

1
2
3
4
5
6
7
8
9
10
11
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

根据要求不允许:

1
INSERT INTO c(c_id, a_id, b_id) VALUES (3,2,1);
1
2
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 <>在这里拨弄

FK约束的默认MATCH SIMPLE行为如下(引用手册):

MATCH SIMPLE allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table.

因此仍允许使用c(b_id)中的NULL值(根据要求:"可选字段")。对于这种特殊情况,FK约束被"禁用"。

我们需要b(a_id, b_id)上的逻辑冗余UNIQUE约束,以允许FK对其进行引用。但是通过确定它位于(a_id, b_id)而不是(b_id, a_id)上,它本身也很有用,它在b(a_id)上提供了一个有用的索引以支持其他FK约束。参见:

  • 复合索引是否也适合在第一个字段上进行查询?

(相应地,c(a_id)上的附加索引通常很有用。)

进一步阅读:

  • 完全匹配,简单匹配和部分匹配之间的区别?
  • 强制执行两个表以外的约束


1
Would LIKE information ON IF this IS possible TO achieve WITHOUT DATABASE triggers

是的,有可能。该机制称为ASSERTION,它在SQL-92标准中定义(尽管未由任何主要的RDBMS实施)。

简而言之,它允许创建多行约束或多表检查约束。

对于PostgreSQL,可以通过将view与WITH CHECK OPTION一起使用并在视图而不是基表上执行操作来模拟它。

WITH CHECK OPTION

This option controls the behavior of automatically updatable views. When this option is specified, INSERT and UPDATE commands on the view will be checked to ensure that new rows satisfy the view-defining condition (that is, the new rows are checked to ensure that they are visible through the view). If they are not, the update will be rejected.

示例:

1
2
3
4
5
6
7
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));

样品插入物:

1
2
3
4
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);

违反条件(将C与两个表上的B个不同的a_id连接)

1
2
UPDATE c SET b_id = 12 WHERE id = 15;;
-- no issues whatsover

创建视图:

1
2
3
4
5
6
7
8
9
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 ;

第二次尝试更新(错误):

1
2
3
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).

尝试使用错误数据(也有错误)的全新插入物

1
2
3
4
5
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 <>小提琴演示


我最终创建了一个触发器,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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"();