Implementação de um relacionamento muitos para muitos com restrições de participação total no SQL

17

Como devo implementar no SQL o cenário descrito no seguinte diagrama de Entidade-Relacionamento?

Relacionamento muitos-para-muitos com restrições de participação total

Como é mostrado, toda Aocorrência de tipo de entidade deve estar relacionada a pelo menos uma B contraparte (indicada pelas linhas de conexão duplas) e vice-versa . Eu sei que devo criar as três tabelas a seguir:

    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)
    );

Mas, e a implementação das restrições de participação total (ou seja, impor que cada instância de Aou Besteja envolvida em pelo menos uma ocorrência de relacionamento com a outra)?

John
fonte

Respostas:

16

Não é fácil fazer no SQL, mas não é impossível. Se você deseja que isso seja aplicado apenas através da DDL, o DBMS deve ter DEFERRABLErestrições implementadas . Isso pode ser feito (e pode ser verificado para funcionar no Postgres, que os implementou):

-- 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
 );

Até aqui está o design "normal", onde todos Apodem ser relacionados a zero, um ou muitos Be todos Bpodem ser relacionados a zero, um ou muitos A.

A restrição "participação total" precisa de restrições na ordem inversa (de Ae B, respectivamente, referência R). Ter FOREIGN KEYrestrições em direções opostas (de X a Y e de Y a X) está formando um círculo (um problema de "galinha e ovo") e é por isso que precisamos de pelo menos uma delas DEFERRABLE. Nesse caso, temos dois círculos ( A -> R -> Ae, B -> R -> Bportanto, precisamos de duas restrições adiadas:

-- 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 ;

Então podemos testar se podemos inserir dados. Observe que INITIALLY DEFERREDnão é necessário. Poderíamos ter definido as restrições como, DEFERRABLE INITIALLY IMMEDIATEmas precisaríamos usar a SET CONSTRAINTSinstrução para adiá-las durante a transação. Em todos os casos, porém, precisamos inserir as tabelas em uma única transação:

-- 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 ;

Testado no SQLfiddle .


Se o DBMS não tiver DEFERRABLErestrições, uma solução alternativa é definir as colunas A (bid)e B (aid)como NULL. Os INSERTprocedimentos / instruções deverão primeiro inserir Ae inserir B(colocando nulos bide aidrespectivamente), depois inserir Re atualizar os valores nulos acima para os valores não nulos relacionados de R.

Com essa abordagem, o DBMS não impõe os requisitos apenas pelo DDL, mas todos os procedimentos INSERT(e UPDATEe DELETEe MERGE) devem ser considerados e ajustados de acordo e os usuários devem ser restritos a usá-los apenas e não ter acesso direto de gravação às tabelas.

Ter círculos nas FOREIGN KEYrestrições não é considerado por muitas práticas recomendadas e por boas razões, sendo a complexidade uma delas. Com a segunda abordagem, por exemplo (com colunas anuláveis), a atualização e a exclusão de linhas ainda terão que ser feitas com código extra, dependendo do DBMS. No SQL Server, por exemplo, você não pode simplesmente colocar ON DELETE CASCADEporque atualizações e exclusões em cascata não são permitidas quando existem círculos FK.

Leia também as respostas desta pergunta relacionada:
Como ter um relacionamento de um para muitos com um filho privilegiado?


Outra terceira abordagem (veja minha resposta na pergunta acima mencionada) é remover completamente os FKs circulares. Assim, mantendo a primeira parte do código (com mesas A, B, Re chaves estrangeiras somente a partir de R para A e B) quase intacta (simplificá-lo), podemos adicionar outra mesa para Aarmazenar o "deve ter um" item relacionado a partir B. Portanto, a A (bid)coluna se move para A_one (bid)O mesmo é feito para o relacionamento reverso de B para 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
 );

A diferença entre a 1ª e a 2ª abordagem é que não há FKs circulares; portanto, as atualizações e exclusões em cascata funcionarão perfeitamente. A aplicação da "participação total" não é apenas da DDL, como na segunda abordagem, e deve ser realizada por procedimentos apropriados ( INSERT/UPDATE/DELETE/MERGE). Uma pequena diferença com a segunda abordagem é que todas as colunas podem ser definidas como não anuláveis.


Outra quarta abordagem (consulte a resposta de @Aaron Bertrand na pergunta acima mencionada) é usar índices exclusivos filtrados / parciais , se estiverem disponíveis no seu DBMS (você precisaria de dois deles, na Rtabela, para este caso). Isso é muito semelhante à 3ª abordagem, exceto que você não precisará das 2 mesas extras. A restrição "participação total" ainda deve ser aplicada por código.

ypercubeᵀᴹ
fonte
A quarta abordagem (um pouco escondida) é realmente perfeita. Como exemplo, consulte postgresql.org/docs/9.6/static/indexes-partial.html Exemplo 11-3 para postgres.
21417 Danilo
@ Danilo Vejo como é perfeito garantir que haja no máximo 1 participação total (com base em algum campo adicional - sucesso no exemplo do postgre). Não consigo ver como é útil garantir que haja pelo menos um sucesso - a questão real neste segmento. Você poderia por favor elaborar?
Alexander Mihailov
3

Você não pode diretamente. Para iniciantes, você não seria capaz de inserir o registro para A sem um B já existente, mas não poderia criar o registro B se não houver um registro A para ele. Existem várias maneiras de aplicá-lo usando coisas como gatilhos - você precisaria verificar todas as inserções e excluir se pelo menos um registro correspondente permanece na tabela de links AB.

Cilíndrico
fonte