Зробити це в SQL непросто, але це неможливо. Якщо ви хочете, щоб це виконувалося лише через DDL, СУБД має мати реалізовані 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 .
Якщо СУБД не має DEFERRABLE
обмежень, одним вирішенням є визначення A (bid)
та B (aid)
стовпців як NULL
. Потім INSERT
процедури / оператори повинні спершу вставити в ( A
і B
ввести нулі bid
і aid
відповідно), потім вставити в, R
а потім оновити нульові значення вище до відповідних ненульових значень з R
.
При такому підході СУБД не виконує вимоги DDL поодинці, але кожну процедуру INSERT
(і UPDATE
та DELETE
та MERGE
) необхідно враховувати та відповідно коригувати, а користувачі повинні обмежуватись лише ними та не мати прямого доступу для запису до таблиць.
Наявність кіл із FOREIGN KEY
обмеженнями багатьма не вважається найкращою практикою і з поважних причин, складність - одна з них. Наприклад, при другому підході (з нульовими стовпцями), оновлення та видалення рядків все одно доведеться робити з додатковим кодом, залежно від СУБД. Наприклад, у SQL Server ви не можете просто поставити, ON DELETE CASCADE
оскільки каскадні оновлення та видалення заборонені, коли є FK-кола.
Будь ласка, прочитайте також відповіді на це пов’язане запитання:
Як мати стосунки з багатьма особами з привілейованою дитиною?
Інший, третій підхід (див. Мою відповідь у вищезгаданому запитанні) полягає в тому, щоб повністю видалити кругові ФК. Таким чином, зберігаючи першу частину коду (з таблицями A
, B
, R
і зовнішні ключі тільки від R до А і В) майже недоторканими ( на насправді спрощує його), ми додамо ще одну таблицю для 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
);
Відмінність від 1-го і 2-го підходу полягає в тому, що кругових FK не існує, тому каскадні оновлення та видалення будуть працювати чудово. Забезпечення "повної участі" відбувається не лише DDL, як у другому підході, а повинно здійснюватися відповідною процедурою ( INSERT/UPDATE/DELETE/MERGE
). Незначна відмінність від другого підходу полягає в тому, що всі стовпці можна визначити не нульовими.
Інший, четвертий підхід (див. Відповідь @Aaron Bertrand у вищезгаданому запитанні) полягає у використанні відфільтрованих / часткових унікальних індексів, якщо вони доступні у вашій СУБД (для цього вам знадобляться два з них у R
таблиці). Це дуже схоже на 3-й підхід, за винятком того, що вам не знадобляться 2 додаткові таблиці. Обмеження "загальної участі" все ще слід застосовувати за кодом.