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 DEFERRABLE
restriçõ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 A
podem ser relacionados a zero, um ou muitos B
e todos B
podem ser relacionados a zero, um ou muitos A
.
A restrição "participação total" precisa de restrições na ordem inversa (de A
e B
, respectivamente, referência R
). Ter FOREIGN KEY
restriçõ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 -> A
e, B -> R -> B
portanto, 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 DEFERRED
não é necessário. Poderíamos ter definido as restrições como, DEFERRABLE INITIALLY IMMEDIATE
mas precisaríamos usar a SET CONSTRAINTS
instruçã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 DEFERRABLE
restrições, uma solução alternativa é definir as colunas A (bid)
e B (aid)
como NULL
. Os INSERT
procedimentos / instruções deverão primeiro inserir A
e inserir B
(colocando nulos bid
e aid
respectivamente), depois inserir R
e 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 UPDATE
e DELETE
e 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 KEY
restriçõ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 CASCADE
porque 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
, R
e chaves estrangeiras somente a partir de R para A e B) quase intacta (simplificá-lo), podemos adicionar outra mesa para A
armazenar 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 R
tabela, 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.