Это не легко сделать в 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
);
Разница по сравнению с первым и вторым подходами заключается в том, что круговые FK отсутствуют, поэтому каскадные обновления и удаления будут работать очень хорошо. Обеспечение "полного участия" осуществляется не одним DDL, как при втором подходе, и должно выполняться с помощью соответствующих процедур ( INSERT/UPDATE/DELETE/MERGE
). Небольшое отличие от второго подхода состоит в том, что все столбцы могут быть определены как обнуляемые.
Другой, четвертый подход (см. Ответ @Aaron Bertrand в вышеупомянутом вопросе) заключается в использовании отфильтрованных / частичных уникальных индексов, если они доступны в вашей СУБД (для этого вам понадобятся два из них, в R
таблице). Это очень похоже на третий подход, за исключением того, что вам не понадобятся 2 дополнительные таблицы. Ограничение «общего участия» все еще должно применяться кодом.