Implementazione di una relazione molti-a-molti con vincoli di partecipazione totale in SQL


17

Come devo implementare in SQL lo scenario rappresentato nel seguente diagramma Entità-Relazione?

Rapporto molti-a-molti con vincoli di partecipazione totale

Come mostrato, ogni Aoccorrenza del tipo di entità deve essere correlata ad almeno una B controparte (indicata dalle doppie linee di connessione) e viceversa . So che dovrei creare le tre tabelle che seguono:

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

Ma che dire dell'implementazione dei vincoli di partecipazione totale (cioè, imporre che ciascuna istanza di uno Ao Bsia coinvolta in un minimo di una relazione che si verifica con l'altra)?

Risposte:


16

Non è facile eseguirlo in SQL ma non è impossibile. Se lo si desidera applicare solo tramite DDL, il DBMS deve avere DEFERRABLEvincoli 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 Apuò essere correlato a zero, uno o molti Be tutti Bpossono essere correlati a zero, uno o molti A.

La restrizione della "partecipazione totale" richiede vincoli nell'ordine inverso (da Ae B, rispettivamente, riferimento R). Avere FOREIGN KEYvincoli 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 -> Ae B -> R -> Bquindi 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 DEFERREDnon è necessario. Avremmo potuto definire i vincoli comeDEFERRABLE INITIALLY IMMEDIATE ma poi avremmo dovuto usare la SET CONSTRAINTSdichiarazione 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 DEFERRABLEvincoli, una soluzione consiste nel definire le colonne A (bid)e B (aid)come NULL. Le INSERTprocedure / dichiarazioni dovranno quindi essere prima inserite in Ae B(inserendo valori nullbid i valori null aidrispettivamente e ), quindi inserire in Re 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 UPDATEed DELETEe 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 KEYvincoli 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 CASCADEperché 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, Re chiavi esterne solo da R a A e B) quasi intatta (in realtà semplificarla), si aggiunge un altro tavolo per Amemorizzare 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 ( Rper 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.


Il quarto approccio (un po 'nascosto) è in realtà perfetto. Ad esempio, vedi postgresql.org/docs/9.6/static/indexes-partial.html Esempio 11-3 per postgres.
Danilo,

@Danilo Vedo che è perfetto per garantire che ci sia un massimo di 1 partecipazione totale (basata su un campo aggiuntivo - successo nell'esempio postgre). Non riesco a vedere come sia d'aiuto garantire che ci sia almeno un successo: la vera domanda in questo thread. Potresti per favore elaborare?
Alexander Mihailov,

3

Non puoi direttamente. Per cominciare, non saresti in grado di inserire il record per A senza una B già esistente, ma non potresti creare il record B se non ci fosse un record. Esistono diversi modi per applicarlo utilizzando elementi come i trigger: è necessario verificare ogni inserimento ed eliminare che almeno un record corrispondente rimanga nella tabella dei collegamenti AB.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.