在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
。
“完全参与”限制需要以相反的顺序进行约束(分别来自A
和B
参考R
)。FOREIGN KEY
在相反的方向上(从X到Y,从Y到X)具有约束,这形成了一个圆(“鸡和蛋”问题),这就是为什么我们至少需要其中一个是DEFERRABLE
。在这种情况下,我们有两个圆(A -> R -> A
和B -> 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
然后程序/语句将不得不第一插入到A
和B
(将空值在bid
和aid
分别地),然后插入到R
,然后更新以上以从相关的非空值的空值R
。
使用这种方法,DBMS不会仅通过DDL来执行要求,而是必须考虑并调整每个过程INSERT
(and UPDATE
and DELETE
and MERGE
),并且必须限制用户仅使用它们,并且不能直接访问表。
FOREIGN KEY
最好的做法并没有考虑到约束中的圈,并且出于充分的原因,复杂性就是其中之一。例如,使用第二种方法(具有可为空的列),仍然必须使用额外的代码来完成更新和删除行,具体取决于DBMS。例如,在SQL Server中,您不能只放置,ON DELETE CASCADE
因为有FK圈子时不允许级联更新和删除。
请同时阅读以下相关问题的答案:
如何与有特权的孩子建立一对多关系?
另一种第三种方法(请参见上述问题的答案)是完全删除循环FK。因此,保持代码的第一部分(附表A
,B
,R
和外键只有来自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
对于这种情况,您需要在表中有两个)。这与第三种方法非常相似,除了您不需要两个额外的表。“完全参与”约束仍然必须通过代码来应用。