Реализация отношения «многие ко многим» с ограничениями общего участия в SQL

17

Как мне реализовать в SQL сценарий, изображенный на следующей схеме Entity-Relationship?

Отношения «многие ко многим» с ограничениями общего участия

Как показано, каждое 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)
    );

Но как насчет реализации общих ограничений участия (т. Е. Обеспечения того, чтобы каждый экземпляр того Aили другого Bучаствовал как минимум в одном вхождении отношений с другим)?

Джон
источник

Ответы:

16

Это не легко сделать в 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, но каждая INSERTUPDATEи 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 дополнительные таблицы. Ограничение «общего участия» все еще должно применяться кодом.

ypercubeᵀᴹ
источник
Четвертый подход (немного скрытый) на самом деле идеален. В качестве примера, смотрите postgresql.org/docs/9.6/static/indexes-partial.html Пример 11-3.
Данила
@Danilo Я вижу, как идеально обеспечить максимальное участие всего 1 (на основе некоторого дополнительного поля - успех в примере postgre). Я не вижу, как это помогает обеспечить хотя бы один успех - актуальный вопрос в этой теме. Не могли бы вы уточнить?
Александр Михайлов
3

Вы не можете напрямую. Начнем с того, что вы не сможете вставить запись для A, если B уже не существует, но вы не можете создать запись B, если для нее нет записи A. Существует несколько способов его применения с использованием таких вещей, как триггеры - вам нужно будет проверять каждую вставку и удалять, чтобы хотя бы одна соответствующая запись оставалась в таблице ссылок AB.

Цилиндрический
источник