Nó không dễ thực hiện trong SQL nhưng nó không phải là không thể. Nếu bạn muốn điều này được thi hành thông qua DDL một mình, DBMS phải thực hiện các DEFERRABLE
ràng buộc. Điều này có thể được thực hiện (và có thể được kiểm tra để hoạt động trong Postgres, đã triển khai chúng):
-- 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
);
Cho đến đây là thiết kế "bình thường", trong đó mọi thứ A
có thể liên quan đến số không, một hoặc nhiều B
và mọi thứ B
có thể liên quan đến số không, một hoặc nhiều A
.
Hạn chế "tổng tham gia" cần các ràng buộc theo thứ tự ngược lại (từ A
và B
tương ứng, tham chiếu R
). Có các FOREIGN KEY
ràng buộc theo hướng ngược lại (từ X đến Y và từ Y đến X) đang tạo thành một vòng tròn (một vấn đề "gà và trứng") và đó là lý do tại sao chúng ta cần ít nhất một trong số chúng DEFERRABLE
. Trong trường hợp này, chúng tôi có hai vòng tròn ( A -> R -> A
và B -> R -> B
vì vậy chúng tôi cần hai ràng buộc có thể bảo vệ:
-- 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 ;
Sau đó chúng tôi có thể kiểm tra rằng chúng tôi có thể chèn dữ liệu. Lưu ý rằng INITIALLY DEFERRED
không cần thiết. Chúng tôi có thể đã xác định các ràng buộc là DEFERRABLE INITIALLY IMMEDIATE
nhưng sau đó chúng tôi phải sử dụng SET CONSTRAINTS
câu lệnh để trì hoãn chúng trong quá trình giao dịch. Trong mọi trường hợp, chúng ta cần phải chèn vào các bảng trong một giao dịch:
-- 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 ;
Đã thử nghiệm tại SQLfiddle .
Nếu DBMS không có các DEFERRABLE
ràng buộc, một cách giải quyết là xác định các cột A (bid)
và B (aid)
cột là NULL
. Các INSERT
thủ tục / câu lệnh sau đó sẽ phải chèn vào trước A
và B
(đặt null vào bid
và aid
tương ứng), sau đó chèn vào R
và sau đó cập nhật các giá trị null ở trên thành các giá trị null không liên quan từ đó R
.
Với phương pháp này, các DBMS không thi hành các yêu cầu của DDL một mình nhưng mỗi INSERT
(và UPDATE
và DELETE
và MERGE
) thủ tục phải được xem xét và điều chỉnh cho phù hợp và người sử dụng phải được giới hạn chỉ sử dụng chúng và không phải trực tiếp ghi vào các bảng.
Có các vòng tròn trong các FOREIGN KEY
ràng buộc không được xem xét bởi nhiều thực tiễn tốt nhất và vì lý do chính đáng, sự phức tạp là một trong số đó. Ví dụ, với cách tiếp cận thứ hai (với các cột không thể), việc cập nhật và xóa các hàng sẽ vẫn phải được thực hiện với mã bổ sung, tùy thuộc vào DBMS. Ví dụ, trong SQL Server, bạn không thể đặt ON DELETE CASCADE
vì các bản cập nhật xếp tầng và xóa không được phép khi có vòng tròn FK.
Xin vui lòng đọc các câu trả lời tại câu hỏi liên quan này:
Làm thế nào để có mối quan hệ một-nhiều với một đứa trẻ đặc quyền?
Một cách tiếp cận thứ 3 khác (xem câu trả lời của tôi trong câu hỏi đã đề cập ở trên) là loại bỏ hoàn toàn các FK tròn. Vì vậy, giữ cho phần đầu tiên của mã (với các bảng A
, B
, R
và các phím nước ngoài chỉ từ R đến A và B) gần như nguyên vẹn (trên thực tế đơn giản hóa nó), chúng tôi thêm một bảng cho A
để lưu trữ các "phải có một" tài liệu liên quan từ B
. Vì vậy, A (bid)
cột di chuyển đến A_one (bid)
Tương tự được thực hiện cho mối quan hệ ngược từ B sang 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
);
Sự khác biệt so với cách tiếp cận thứ 1 và thứ 2 là không có FK tròn, do đó các cập nhật xếp tầng và xóa sẽ hoạt động tốt. Việc thực thi "tổng tham gia" không chỉ bằng DDL, như trong cách tiếp cận thứ 2, và phải được thực hiện bằng các thủ tục thích hợp ( INSERT/UPDATE/DELETE/MERGE
). Một sự khác biệt nhỏ với cách tiếp cận thứ 2 là tất cả các cột có thể được xác định không phải là null.
Một cách tiếp cận thứ 4 khác (xem câu trả lời của @Aaron Bertrand trong câu hỏi đã đề cập ở trên) là sử dụng các chỉ mục duy nhất được lọc / một phần , nếu chúng có sẵn trong DBMS của bạn ( R
trong trường hợp này, bạn cần hai trong số chúng, trong trường hợp này). Điều này rất giống với cách tiếp cận thứ 3, ngoại trừ việc bạn sẽ không cần thêm 2 bảng. Ràng buộc "tổng tham gia" vẫn phải được áp dụng theo mã.