Implementación de una relación de muchos a muchos con restricciones de participación total en SQL

17

¿Cómo debo implementar en SQL el escenario representado en el siguiente diagrama de Entidad-Relación?

Relación de muchos a muchos con restricciones de participación total

Como se muestra, cada Aocurrencia de tipo de entidad debe estar relacionada con al menos una B contraparte (indicada por las líneas de conexión dobles), y viceversa . Sé que debería crear las tres tablas que siguen:

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

Pero, ¿qué pasa con la aplicación de los totales de participación restricciones (es decir, la aplicación que cada instancia de cualquiera Ao Bparticipa en un mínimo de una ocurrencia relación con el otro)?

John
fuente

Respuestas:

16

No es fácil de hacer en SQL, pero no es imposible. Si desea que esto se aplique solo a través de DDL, el DBMS debe tener DEFERRABLErestricciones implementadas . Esto podría hacerse (y puede comprobarse para que funcione en Postgres, que los ha implementado):

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

Hasta aquí está el diseño "normal", donde cada uno Apuede estar relacionado con cero, uno o muchos By cada uno Bpuede estar relacionado con cero, uno o muchos A.

La restricción de "participación total" necesita restricciones en el orden inverso (desde Ay Brespectivamente, referenciando R). Tener FOREIGN KEYrestricciones en direcciones opuestas (de X a Y y de Y a X) está formando un círculo (un problema de "huevo y gallina") y es por eso que al menos necesitamos uno de ellos DEFERRABLE. En este caso tenemos dos círculos ( A -> R -> Ay B -> R -> Bpor eso necesitamos dos restricciones diferibles:

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

Entonces podemos probar que podemos insertar datos. Tenga en cuenta que INITIALLY DEFERREDno es necesario. Podríamos haber definido las restricciones como DEFERRABLE INITIALLY IMMEDIATEpero luego tendríamos que usar la SET CONSTRAINTSdeclaración para diferirlas durante la transacción. Sin embargo, en todos los casos, necesitamos insertar en las tablas en una sola transacción:

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

Probado en SQLfiddle .


Si el DBMS no tiene DEFERRABLErestricciones, una solución es definir las columnas A (bid)y B (aid)como NULL. Los INSERTprocedimientos / declaraciones tendrán que insertarse primero en Ay B(poner nulos en bidy aidrespectivamente), luego insertar Ry luego actualizar los valores nulos anteriores a los valores no nulos relacionados de R.

Con este enfoque, el DBMS no cumplir los requisitos de DDL solo, sino todos los INSERT(e UPDATEy DELETEe MERGE) procedimiento tiene que ser considerado y ajustarse en consecuencia y los usuarios tienen que limitarse a utilizar únicamente ellos y no tienen acceso directo de escritura en las tablas.

Tener círculos en las FOREIGN KEYrestricciones no es considerado por muchos como la mejor práctica y por buenas razones, la complejidad es una de ellas. Con el segundo enfoque, por ejemplo (con columnas anulables), la actualización y eliminación de filas aún tendrá que hacerse con un código adicional, dependiendo del DBMS. En SQL Server, por ejemplo, no puede simplemente poner ON DELETE CASCADEporque las actualizaciones y eliminaciones en cascada no están permitidas cuando hay círculos FK.

Lea también las respuestas a esta pregunta relacionada:
¿Cómo tener una relación de uno a muchos con un niño privilegiado?


Otro tercer enfoque (ver mi respuesta en la pregunta mencionada anteriormente) es eliminar completamente los FK circulares. Por lo tanto, mantener la primera parte del código (con mesas A, B, Ry las claves externas sólo de R a A y B) casi intacta (en realidad, simplificándolo), que añaden otra mesa para Aalmacenar el "debe tener un" elemento relacionado de B. Entonces, la A (bid)columna se mueve a A_one (bid)Lo mismo se hace para la relación inversa de B a 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 diferencia con respecto al primer y segundo enfoque es que no hay FK circulares, por lo que las actualizaciones y eliminaciones en cascada funcionarán bien. La aplicación de la "participación total" no es solo por DDL, como en el segundo enfoque, y debe hacerse mediante los procedimientos apropiados ( INSERT/UPDATE/DELETE/MERGE). Una pequeña diferencia con el segundo enfoque es que todas las columnas se pueden definir no anulables.


Otro cuarto enfoque (consulte la respuesta de @Aaron Bertrand en la pregunta mencionada anteriormente) es usar índices únicos filtrados / parciales , si están disponibles en su DBMS (necesitaría dos de ellos, en la Rtabla, para este caso). Esto es muy similar al tercer enfoque, excepto que no necesitará las 2 tablas adicionales. La restricción de "participación total" aún debe aplicarse por código.

ypercubeᵀᴹ
fuente
El cuarto enfoque (un poco escondido) es realmente perfecto. Como ejemplo, vea postgresql.org/docs/9.6/static/indexes-partial.html Ejemplo 11-3 para postgres.
Danilo
@Danilo Veo cómo es perfecto asegurarse de que haya una participación total máxima de 1 (basado en algún campo adicional: éxito en el ejemplo de postgre). No puedo ver cómo es útil asegurar que haya al menos un éxito: la pregunta real en este hilo. ¿Podrías por favor elaborar?
Alexander Mihailov
3

No puedes directamente. Para empezar, no podría insertar el registro para A sin una B ya existente, pero no podría crear el registro B si no hay un registro A para él. Hay varias formas de aplicarlo utilizando elementos como los desencadenantes: debería verificar cada inserción y eliminar que al menos un registro correspondiente permanezca en la tabla de enlaces AB.

Cilíndrico
fuente