Non è facile eseguirlo in SQL ma non è impossibile. Se lo si desidera applicare solo tramite DDL, il DBMS deve avere DEFERRABLE
vincoli implementati . Questo potrebbe essere fatto (e può essere verificato per funzionare in Postgres, che li ha implementati):
-- 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
);
Fino a qui è il design "normale", dove ogni A
può essere correlato a zero, uno o molti B
e tutti B
possono essere correlati a zero, uno o molti A
.
La restrizione della "partecipazione totale" richiede vincoli nell'ordine inverso (da A
e B
, rispettivamente, riferimento R
). Avere FOREIGN KEY
vincoli in direzioni opposte (da X a Y e da Y a X) sta formando un cerchio (un problema di "pollo e uovo") ed è per questo che abbiamo bisogno di uno di loro almeno per essereDEFERRABLE
. In questo caso abbiamo due cerchi ( A -> R -> A
e B -> R -> B
quindi abbiamo bisogno di due vincoli differibili:
-- 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 ;
Quindi possiamo verificare che possiamo inserire dati. Si noti che INITIALLY DEFERRED
non è necessario. Avremmo potuto definire i vincoli comeDEFERRABLE INITIALLY IMMEDIATE
ma poi avremmo dovuto usare la SET CONSTRAINTS
dichiarazione per rimandarli durante la transazione. In ogni caso, tuttavia, dobbiamo inserire nelle tabelle in una singola transazione:
-- 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 ;
Testato a SQLfiddle .
Se il DBMS non ha DEFERRABLE
vincoli, una soluzione consiste nel definire le colonne A (bid)
e B (aid)
come NULL
. Le INSERT
procedure / dichiarazioni dovranno quindi essere prima inserite in A
e B
(inserendo valori nullbid
i valori null aid
rispettivamente e ), quindi inserire in R
e quindi aggiornare i valori null sopra ai relativi valori non null da R
.
Con questo approccio, il DBMS non applica i requisiti solo da DDL ma tutti INSERT
(e UPDATE
ed DELETE
e MERGE
) procedura deve essere considerato e adeguato di conseguenza e gli utenti devono essere limitato a utilizzare solo loro e non hanno accesso in scrittura direttamente ai tavoli.
Avere circoli nei FOREIGN KEY
vincoli non è considerato da molti la migliore pratica e, per buoni motivi, la complessità è uno di questi. Ad esempio, con il secondo approccio (con colonne nullable), l'aggiornamento e l'eliminazione delle righe dovranno ancora essere eseguiti con codice aggiuntivo, a seconda del DBMS. In SQL Server, ad esempio, non puoi semplicemente inserire ON DELETE CASCADE
perché aggiornamenti a cascata ed eliminazioni non sono consentiti quando ci sono cerchi FK.
Leggi anche le risposte a questa domanda correlata:
come avere una relazione uno-a-molti con un figlio privilegiato?
Un altro terzo approccio (vedere la mia risposta nella domanda sopra menzionata) è rimuovere completamente gli FK circolari. Quindi, mantenendo la prima parte del codice (con tavoli A
, B
, R
e chiavi esterne solo da R a A e B) quasi intatta (in realtà semplificarla), si aggiunge un altro tavolo per A
memorizzare il "deve avere un" elemento correlato da B
. Quindi, la A (bid)
colonna si sposta su A_one (bid)
Lo stesso viene fatto per la relazione inversa da B ad 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
);
La differenza rispetto al primo e al secondo approccio è che non ci sono FK circolari, quindi gli aggiornamenti e le eliminazioni a cascata funzioneranno perfettamente. L'applicazione della "partecipazione totale" non è da sola da parte del DDL, come nel secondo approccio, e deve essere effettuata mediante procedure appropriate (INSERT/UPDATE/DELETE/MERGE
). Una piccola differenza con il secondo approccio è che tutte le colonne possono essere definite non annullabili.
Un altro quarto approccio (vedi la risposta di @Aaron Bertrand nella domanda sopra menzionata) consiste nell'utilizzare indici univoci filtrati / parziali , se sono disponibili nel tuo DBMS ( R
per questo caso ne avresti bisogno due nella tabella). Questo è molto simile al terzo approccio, tranne per il fatto che non avrai bisogno dei 2 tavoli extra. Il vincolo "partecipazione totale" deve ancora essere applicato dal codice.