web-dev-qa-db-fra.com

Implémentation d'une relation plusieurs-à-plusieurs avec des contraintes de participation totale dans SQL

Comment dois-je implémenter en SQL le scénario décrit dans le diagramme Entité-Relation suivant?

Many-to-many relationship with total participation constraints

Comme indiqué, chaque occurrence de type d'entité A doit être liée à au moins uneB homologue (indiquée par les doubles lignes de connexion) et vice versa . Je sais que je devrais créer les trois tableaux suivants:

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

Mais, qu'en est-il de la mise en œuvre des contraintes participation totale (c'est-à-dire application que chaque instance de A ou B est impliqué dans au moins une occurrence de relation avec l'autre)?

17
John

Ce n'est pas facile à faire en SQL mais ce n'est pas impossible. Si vous souhaitez que cela soit appliqué uniquement via DDL, le SGBD doit avoir implémenté des contraintes DEFERRABLE. 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ù chaque A peut être lié à zéro, un ou plusieurs B et chaque B peut être lié à zéro, un ou plusieurs A.

La restriction de "participation totale" nécessite des contraintes dans l'ordre inverse (de A et B respectivement, référençant R). Avoir des contraintes FOREIGN KEY Dans des directions opposées (de X à Y et de Y à X) forme un cercle (un problème de "poulet et œuf") et c'est pourquoi nous avons besoin que l'un d'entre eux 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 il nous faudrait alors utiliser l'instruction SET CONSTRAINTS 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 contraintes DEFERRABLE, une solution consiste à définir les colonnes A (bid) et B (aid) comme NULL. Les procédures/instructions INSERT devront alors d'abord être insérées dans A et B (en mettant respectivement des valeurs nulles dans bid et aid), puis insérez dans R puis mettez à jour les valeurs nulles ci-dessus vers les valeurs non nulles connexes de R.

Avec cette approche, le SGBD n'applique pas les exigences uniquement par DDL mais toutes les procédures INSERT (et UPDATE et DELETE et MERGE) doivent être prises en compte et ajustés en conséquence et les utilisateurs doivent être limités à utiliser uniquement eux et ne pas avoir un accès direct en écriture aux tables.

La présence de cercles dans les contraintes FOREIGN KEY 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. Donc, garder la première partie du code (avec les tables A, B, R et les clés étrangères uniquement de R à A et B) presque intacte (en fait la simplifier) , nous ajoutons une autre table pour A pour stocker l'élément connexe "doit en avoir un" de B. Ainsi, la colonne A (bid) se déplace vers A_one (bid) La même chose est faite pour la relation inverse de B vers 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 première et deuxiè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 @ réponse d'Aaron Bertrand dans la question mentionnée ci-dessus) consiste à utiliser index uniques filtrés/partiels , s'ils sont disponibles dans votre SGBD (vous en aurez besoin de deux, dans la table R, 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.

16
ypercubeᵀᴹ

Vous ne pouvez pas directement. Pour commencer, vous ne seriez pas en mesure d'insérer l'enregistrement pour A sans un B déjà existant, mais vous ne pourriez pas créer l'enregistrement B s'il n'y a pas d'enregistrement A pour lui. Il existe plusieurs façons de l'appliquer en utilisant des éléments tels que les déclencheurs - vous devez vérifier à chaque insertion et supprimer qu'au moins un enregistrement correspondant reste dans la table de liens AB.

3
Cylindric