SQL中具有总参与约束的多对多关系的实现

Joh*_*ohn 17 erd database-design

我应该如何在 SQL 中实现以下实体关系图中描述的场景?

具有总参与约束的多对多关系

如图所示,每个A实体类型出现都必须与至少一个 B对应项(由双连接线表示)相关,反之亦然。我知道我应该创建以下三个表:

    CREATE TABLE A
    (
        a INT NOT NULL,
        CONSTRAINT A_PK PRIMARY KEY (a)
    );

    CREATE TABLE B
    (
        b INT NOT NULL,
        CONSTRAINT B_PK PRIMARY KEY (b)
    );

    CREATE TABLE R
    (
        a INT NOT NULL,
        b INT NOT NULL,
        CONSTRAINT R_PK      PRIMARY KEY (a, b),
        CONSTRAINT R_to_A_FK FOREIGN KEY (a)
            REFERENCES A (a),
        CONSTRAINT R_to_B_FK FOREIGN KEY (b)
            REFERENCES B (b)
    );
Run Code Online (Sandbox Code Playgroud)

但是,如何实现总参与约束(即,强制执行其中之一的每个实例A至少B涉及与另一个关系的发生)?

ype*_*eᵀᴹ 16

在 SQL 中做到这一点并不容易,但并非不可能。如果您希望仅通过 DDL 强制执行此操作,则 DBMS 必须实施DEFERRABLE约束。这可以完成(并且可以检查在 Postgres 中工作,已经实现了它们):

-- lets create first the 2 tables, A and B:
CREATE TABLE a 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT a_pk PRIMARY KEY (aid) 
 );

CREATE TABLE b 
( bid INT NOT NULL,
  aid INT NOT NULL,
  CONSTRAINT b_pk PRIMARY KEY (bid) 
 );

-- then table R:
CREATE TABLE r 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT r_pk PRIMARY KEY (aid, bid),
  CONSTRAINT a_r_fk FOREIGN KEY (aid) REFERENCES a,  
  CONSTRAINT b_r_fk FOREIGN KEY (bid) REFERENCES b
 );
Run Code Online (Sandbox Code Playgroud)

到这里是“正常”的设计,其中每一个A可以与零,一个或多个B,每个B可与零,一个或多个A

“总参与”限制需要相反顺序的约束(分别来自AB,引用R)。有FOREIGN KEY相反的方向约束(从X到Y和从Y到X)正在形成一个圆圈(一个“鸡和蛋”的问题),以及为什么我们需要他们的一个最起码要的DEFERRABLE。在这种情况下,我们有两个圆(A -> R -> AB -> R -> B,所以我们需要两个延迟约束:

-- then we add the 2 constraints that enforce the "total participation":
ALTER TABLE a
  ADD CONSTRAINT r_a_fk FOREIGN KEY (aid, bid) REFERENCES r 
    DEFERRABLE INITIALLY DEFERRED ;

ALTER TABLE b
  ADD CONSTRAINT r_b_fk FOREIGN KEY (aid, bid) REFERENCES r 
    DEFERRABLE INITIALLY DEFERRED ;
Run Code Online (Sandbox Code Playgroud)

然后我们可以测试我们可以插入数据。请注意,INITIALLY DEFERRED不需要。我们可以将约束定义为,DEFERRABLE INITIALLY IMMEDIATE但是我们必须SET CONSTRAINTS在事务期间使用该语句来推迟它们。但是,在每种情况下,我们都需要在单个事务中插入表中:

-- insert data 
BEGIN TRANSACTION ;
    INSERT INTO a (aid, bid)
    VALUES
      (1, 1),    (2, 5),
      (3, 7),    (4, 1) ;

    INSERT INTO b (aid, bid)
    VALUES
      (1, 1),    (1, 2),
      (2, 3),    (2, 4),
      (2, 5),    (3, 6),
      (3, 7) ;

    INSERT INTO r (aid, bid)
    VALUES
      (1, 1),    (1, 2),
      (2, 3),    (2, 4),
      (2, 5),    (3, 6),
      (3, 7),    (4, 1),
      (4, 2),    (4, 7) ; 
 END ;
Run Code Online (Sandbox Code Playgroud)

SQLfiddle测试。


如果 DBMS 没有DEFERRABLE约束,一种解决方法是将A (bid)B (aid)列定义为NULL。然后,INSERT过程/语句必须首先插入AB(分别在bid和 中放入空值aid),然后插入R并更新上面的空值到来自 的相关非空值R

使用这种方法,DBMS 不会单独通过 DDL 强制执行要求,而是必须考虑并相应地调整每个INSERT(和UPDATEDELETEMERGE)过程,并且必须限制用户仅使用它们并且不能直接对表进行写访问。

FOREIGN KEY许多最佳实践并不认为在约束中使用圆圈,并且出于充分的理由,复杂性就是其中之一。例如,使用第二种方法(使用可空列),更新和删除行仍然需要使用额外的代码来完成,具体取决于 DBMS。以SQL Server为例,你不能只放,ON DELETE CASCADE因为当有FK圈时不允许级联更新和删除。

另请阅读此相关问题的答案:
如何与特权儿童建立一对多关系?


另一种第三种方法(请参阅我在上述问题中的回答)是完全删除圆形 FK。因此,保持代码的第一部分(附表ABR和外键只有来自R A和B)几乎完好无损(实际上简化了它),我们添加另一个表A以“必须有一个”相关项目的存储B。因此,该A (bid)列移动到A_one (bid)B 到 A 的反向关系也是如此:

CREATE TABLE a 
( aid INT NOT NULL,
  CONSTRAINT a_pk PRIMARY KEY (aid) 
 );

CREATE TABLE b 
( bid INT NOT NULL,
  CONSTRAINT b_pk PRIMARY KEY (bid) 
 );

-- then table R:
CREATE TABLE r 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT r_pk PRIMARY KEY (aid, bid),
  CONSTRAINT a_r_fk FOREIGN KEY (aid) REFERENCES a,  
  CONSTRAINT b_r_fk FOREIGN KEY (bid) REFERENCES b
 );

CREATE TABLE a_one 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT a_one_pk PRIMARY KEY (aid),
  CONSTRAINT r_a_fk FOREIGN KEY (aid, bid) REFERENCES r
 );

CREATE TABLE b_one
( bid INT NOT NULL,
  aid INT NOT NULL,
  CONSTRAINT b_one_pk PRIMARY KEY (bid),
  CONSTRAINT r_b_fk FOREIGN KEY (aid, bid) REFERENCES r
 );
Run Code Online (Sandbox Code Playgroud)

第一种和第二种方法的区别在于没有循环 FK,因此级联更新和删除将正常工作。“完全参与”的实施不是 DDL 单独执行的,如第二种方法,必须通过适当的程序 ( INSERT/UPDATE/DELETE/MERGE)来完成。与第二种方法的一个细微差别是所有列都可以定义为不可为空。


另一种第四种方法(参见@Aaron Bertrand在上述问题中的回答)是使用过滤/部分唯一索引,如果它们在您的 DBMS 中可用(R对于这种情况,您需要其中两个,在表中)。这与第 3 种方法非常相似,只是您不需要 2 个额外的表。“总参与”约束仍需通过代码应用。


Cyl*_*ric 5

你不能直接。对于初学者来说,如果没有 B 已存在,您将无法插入 A 记录,但如果没有 A 记录,则无法创建 B 记录。有多种方法可以使用触发器之类的东西来强制执行它 - 您必须检查每次插入和删除时,AB 链接表中至少保留一个相应的记录。