Ce n'est pas facile à faire en SQL mais ce n'est pas impossible. Si vous voulez que cela soit appliqué via DDL seul, le SGBD doit avoir implémenté des DEFERRABLEcontraintes. Cela pourrait être fait (et peut être vérifié pour fonctionner dans Postgres, qui les a implémentés):
-- 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
);
Jusqu'ici est la conception "normale", où tout Apeut être lié à zéro, un ou plusieurs Bet chacun Bpeut être lié à zéro, un ou plusieurs A.
La restriction de "participation totale" nécessite des contraintes dans l'ordre inverse (à partir Aet Brespectivement du référencement R). Avoir des FOREIGN KEYcontraintes dans des directions opposées (de X à Y et de Y à X) forme un cercle (un problème de "poule et œuf") et c'est pourquoi nous avons besoin que l'une d'entre elles soit au moins DEFERRABLE. Dans ce cas, nous avons deux cercles ( A -> R -> Aet B -> R -> Bnous avons donc besoin de deux contraintes reportables:
-- 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 ;
Ensuite, nous pouvons tester que nous pouvons insérer des données. Notez que le INITIALLY DEFERREDn'est pas nécessaire. Nous aurions pu définir les contraintes comme DEFERRABLE INITIALLY IMMEDIATEmais nous devions alors utiliser l' SET CONSTRAINTSinstruction pour les différer pendant la transaction. Dans tous les cas cependant, nous devons insérer dans les tables en une seule transaction:
-- 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 ;
Testé à SQLfiddle .
Si le SGBD n'a pas de DEFERRABLEcontraintes, une solution consiste à définir les colonnes A (bid)et B (aid)comme NULL. Les INSERTprocédures / instructions devront ensuite d'abord insérer dans Aet B(mettre des valeurs nulles dans bidet aidrespectivement), puis insérer dans Rpuis mettre à jour les valeurs nulles ci-dessus pour les valeurs non nulles connexes de R.
Avec cette approche, le SGBD n'applique pas les exigences uniquement par DDL mais chaque procédure INSERT(et UPDATEet DELETEet MERGE) doit être considérée et ajustée en conséquence et les utilisateurs doivent être limités à les utiliser uniquement et ne pas avoir un accès direct en écriture aux tables.
La présence de cercles dans les FOREIGN KEYcontraintes n'est pas considérée par beaucoup comme la meilleure pratique et pour de bonnes raisons, la complexité étant l'une d'entre elles. Avec la deuxième approche par exemple (avec des colonnes nullables), la mise à jour et la suppression des lignes devront toujours être effectuées avec du code supplémentaire, selon le SGBD. Dans SQL Server par exemple, vous ne pouvez pas simplement mettre ON DELETE CASCADEcar les mises à jour et les suppressions en cascade ne sont pas autorisées lorsqu'il y a des cercles FK.
Veuillez également lire les réponses à cette question connexe:
Comment avoir une relation un-à-plusieurs avec un enfant privilégié?
Une autre troisième approche (voir ma réponse dans la question ci-dessus) consiste à supprimer complètement les FK circulaires. Ainsi, en gardant la première partie du code (avec des tables A, B, Ret les clés étrangères seulement de R à A et B) presque intact ( en simplifiant réellement), nous ajoutons une autre table pour Astocker le « doit avoir un » élément lié de B. Ainsi, la A (bid)colonne se déplace vers A_one (bid)La même chose se fait pour la relation inverse de 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
);
La différence par rapport aux 1ère et 2ème approches est qu'il n'y a pas de FK circulaires, donc les mises à jour et suppressions en cascade fonctionneront très bien. L'application de la "participation totale" ne se fait pas uniquement par DDL, comme dans la deuxième approche, et doit être effectuée par des procédures appropriées ( INSERT/UPDATE/DELETE/MERGE). Une différence mineure avec la 2ème approche est que toutes les colonnes peuvent être définies comme non nulles.
Une autre, 4ème approche (voir la réponse de @Aaron Bertrand dans la question ci-dessus) consiste à utiliser des index uniques filtrés / partiels , s'ils sont disponibles dans votre SGBD (vous en auriez besoin de deux, dans le Rtableau, dans ce cas). Ceci est très similaire à la 3ème approche, sauf que vous n'aurez pas besoin des 2 tables supplémentaires. La contrainte de "participation totale" doit encore être appliquée par code.