Implementierung einer Viele-zu-Viele-Beziehung mit Einschränkungen der Gesamtbeteiligung in SQL

17

Wie sollte ich das im folgenden Entity-Relationship-Diagramm dargestellte Szenario in SQL implementieren?

Viele-zu-viele-Beziehung mit Einschränkungen der Gesamtbeteiligung

Wie gezeigt, muss jedes AVorkommen eines Entitätstyps mit mindestens einemB Gegenstück verknüpft sein (angezeigt durch die doppelten Verbindungslinien) und umgekehrt . Ich weiß, ich sollte die drei folgenden Tabellen erstellen:

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

Aber was ist mit der Umsetzung der gesamten Teilnahmebedingungen (dh Durchsetzung , dass jede Instanz von entweder Aoder Bbeteiligt ist mindestens eine mit dem anderen Beziehung Vorkommen)?

John
quelle

Antworten:

16

In SQL ist das nicht einfach, aber nicht unmöglich. Wenn dies nur durch DDL erzwungen werden soll, muss das DBMS DEFERRABLEEinschränkungen implementiert haben . Dies könnte geschehen (und kann überprüft werden, um in Postgres zu funktionieren, das sie implementiert hat):

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

Bis hierher gibt es das "normale" Design, bei dem jedes Aauf Null, eins oder viele Bund jedes Bauf Null, eins oder viele bezogen werden kann A.

Die „Gesamt Teilnahme“ Beschränkung muss Einschränkungen in der umgekehrten Reihenfolge (von Aund Bbzw. Referenzierung R). Mit FOREIGN KEYEinschränkungen in entgegengesetzten Richtungen (von X nach Y und von Y bis X) einen Kreises (eine „Huhn und Ei“ -Problem) , und dass die Bildung , warum wir eine von ihnen zumindest sein müssen DEFERRABLE. In diesem Fall haben wir zwei Kreise ( A -> R -> Aund B -> R -> Bdaher brauchen wir zwei aufschiebbare Bedingungen:

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

Dann können wir testen, ob wir Daten einfügen können. Beachten Sie, dass das INITIALLY DEFERREDnicht benötigt wird. Wir hätten die Einschränkungen definieren können als, DEFERRABLE INITIALLY IMMEDIATEaber dann müssten wir die SET CONSTRAINTSAnweisung verwenden, um sie während der Transaktion aufzuschieben. In jedem Fall müssen wir jedoch in einer einzigen Transaktion in die Tabellen einfügen:

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

Getestet bei SQLfiddle .


Wenn das DBMS keine DEFERRABLEEinschränkungen aufweist, besteht eine Problemumgehung darin, die Spalten A (bid)und B (aid)als zu definieren NULL. Die INSERTVerfahren / Anweisungen werden dann zum ersten Einsatz haben in Aund B(NULL - Werten bei der Umsetzung bidund aidrespectively), dann in einfügen Rund dann über den Zusammenhang keine Nullwerte aus den Nullwert aktualisieren R.

Bei diesem Ansatz erzwingt das DBMS nicht nur die Anforderungen von DDL, sondern es muss jede INSERT(und UPDATEund DELETEund MERGE) Prozedur entsprechend berücksichtigt und angepasst werden, und Benutzer müssen darauf beschränkt werden, nur diese zu verwenden, und haben keinen direkten Schreibzugriff auf die Tabellen.

Kreise in den FOREIGN KEYBeschränkungen zu haben, wird von vielen nicht als die beste Praxis angesehen, und aus guten Gründen ist die Komplexität eine davon. Beim zweiten Ansatz (z. B. mit nullwertfähigen Spalten) muss das Aktualisieren und Löschen von Zeilen je nach DBMS mit zusätzlichem Code ausgeführt werden. In SQL Server zum Beispiel können Sie nicht einfach setzen, ON DELETE CASCADEda kaskadierende Aktualisierungen und Löschvorgänge nicht zulässig sind, wenn FK-Kreise vorhanden sind.

Bitte lesen Sie auch die Antworten zu dieser verwandten Frage:
Wie kann man eine Eins-zu-Viele-Beziehung zu einem privilegierten Kind haben?


Ein anderer, dritter Ansatz (siehe meine Antwort in der oben genannten Frage) besteht darin, die kreisförmigen FKs vollständig zu entfernen. So, den ersten Teil des Codes zu halten (mit Tabellen A, B, Rund Fremdschlüssel nur von R A und B) fast vollständig erhalten (vereinfacht es tatsächlich), fügen wir eine weitere Tabelle für Adie zum Speichern von „muss man hat“ verwandten Artikel B. Die A (bid)Spalte verschiebt sich also zu. A_one (bid)Dasselbe gilt für die umgekehrte Beziehung von B zu 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
 );

Der Unterschied zum 1. und 2. Ansatz besteht darin, dass es keine zirkulären FKs gibt, sodass kaskadierende Aktualisierungen und Löschvorgänge problemlos funktionieren. Die Durchsetzung der "vollständigen Beteiligung" erfolgt nicht wie im zweiten Ansatz allein durch DDL und muss durch geeignete Verfahren erfolgen ( INSERT/UPDATE/DELETE/MERGE). Ein kleiner Unterschied zum 2. Ansatz besteht darin, dass alle Spalten nicht nullwertfähig definiert werden können.


Ein anderer, vierter Ansatz (siehe @Aaron Bertrands Antwort in der oben genannten Frage) besteht darin, gefilterte / teilweise eindeutige Indizes zu verwenden, sofern diese in Ihrem DBMS verfügbar sind ( Rfür diesen Fall benötigen Sie zwei davon in der Tabelle). Dies ist dem 3. Ansatz sehr ähnlich, außer dass Sie die 2 zusätzlichen Tabellen nicht benötigen. Die Einschränkung "Gesamtteilnahme" muss noch per Code angewendet werden.

ypercubeᵀᴹ
quelle
Der 4. Ansatz (etwas versteckt) ist eigentlich perfekt. Ein Beispiel finden Sie unter postgresql.org/docs/9.6/static/indexes-partial.html Beispiel 11-3 für postgres.
Danilo
@Danilo Ich sehe, dass es perfekt ist, um sicherzustellen, dass es maximal 1 Gesamtteilnahme gibt (basierend auf einem zusätzlichen Feld - Erfolg im Postgre-Beispiel). Ich kann nicht sehen, wie es hilft, sicherzustellen, dass es mindestens einen Erfolg gibt - die eigentliche Frage in diesem Thread. Könnten Sie bitte näher darauf eingehen?
Alexander Mihailov
3

Sie können nicht direkt. Für den Anfang wäre es nicht möglich, den Datensatz für A einzufügen, ohne dass bereits ein B vorhanden ist, aber Sie könnten den B-Datensatz nicht erstellen, wenn es keinen A-Datensatz dafür gibt. Es gibt verschiedene Möglichkeiten, dies durch Trigger zu erzwingen. Sie müssten bei jedem Einfügen und Löschen überprüfen, ob mindestens ein entsprechender Datensatz in der AB-Verknüpfungstabelle verbleibt.

Zylindrisch
quelle