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 DEFERRABLE
contraintes. 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 A
peut être lié à zéro, un ou plusieurs B
et chacun B
peut être lié à zéro, un ou plusieurs A
.
La restriction de "participation totale" nécessite des contraintes dans l'ordre inverse (à partir A
et B
respectivement du référencement R
). Avoir des FOREIGN KEY
contraintes 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 -> A
et B -> R -> B
nous 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 DEFERRED
n'est pas nécessaire. Nous aurions pu définir les contraintes comme DEFERRABLE INITIALLY IMMEDIATE
mais nous devions alors utiliser l' SET CONSTRAINTS
instruction 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 DEFERRABLE
contraintes, une solution consiste à définir les colonnes A (bid)
et B (aid)
comme NULL
. Les INSERT
procédures / instructions devront ensuite d'abord insérer dans A
et B
(mettre des valeurs nulles dans bid
et aid
respectivement), puis insérer dans R
puis 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 UPDATE
et DELETE
et 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 KEY
contraintes 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 CASCADE
car 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
, R
et les clés étrangères seulement de R à A et B) presque intact ( en simplifiant réellement), nous ajoutons une autre table pour A
stocker 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 R
tableau, 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.