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


17

我应该如何在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)
    );

但是,怎么样的执行总参与约束(即,执行的是每一个的任一情况AB涉及最少一个的关系发生与其他)?

Answers:


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
 );

到这里为止是“正常”设计,其中每个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 ;

然后,我们可以测试是否可以插入数据。请注意,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 ;

SQLfiddle中测试。


如果DBMS没有DEFERRABLE约束,则一种解决方法是将A (bid)B (aid)列定义为NULL。所述INSERT然后程序/语句将不得不第一插入到AB(将空值在bidaid分别地),然后插入到R,然后更新以上以从相关的非空值的空值R

使用这种方法,DBMS不会仅通过DDL来执行要求,而是必须考虑并调整每个过程INSERT(and UPDATEand DELETEand MERGE),并且必须限制用户仅使用它们,并且不能直接访问表。

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
 );

与第一种方法和第二种方法的区别在于,没有循环FK,因此级联更新和删除将可以正常工作。不像第二种方法那样,仅由DDL强制执行“完全参与”,而必须通过适当的程序(INSERT/UPDATE/DELETE/MERGE)进行。与第二种方法的一个次要区别是,可以将所有列定义为不可为空。


另一种第四种方法(请参见上述问题中的@Aaron Bertrand的答案)是使用过滤的/部分的唯一索引(如果它们在DBMS中可用)(R对于这种情况,您需要在表中有两个)。这与第三种方法非常相似,除了您不需要两个额外的表。“完全参与”约束仍然必须通过代码来应用。


第四种方法(有点隐蔽)实际上是完美的。例如,请参阅postgresql.org/docs/9.6/static/indexes-partial.html示例11-3。
达尼洛

@Danilo我看到确保有最多1个总参与是非常完美的(基于一些其他字段-postgre示例中的成功)。我看不到确保至少成功是有帮助的-这个线程中的实际问题。您能详细说明一下吗?
亚历山大·米海洛夫

3

你不能直接。对于初学者,如果没有B,则无法插入A的记录,但是如果没有A记录,则无法创建B记录。有多种使用触发器之类的方法来强制执行它的方法-您必须检查每个插入并删除AB链接表中至少有一个对应的记录。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.