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 DEFERRABLE
restricciones 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 A
puede estar relacionado con cero, uno o muchos B
y cada uno B
puede estar relacionado con cero, uno o muchos A
.
La restricción de "participación total" necesita restricciones en el orden inverso (desde A
y B
respectivamente, referenciando R
). Tener FOREIGN KEY
restricciones 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 -> A
y B -> R -> B
por 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 DEFERRED
no es necesario. Podríamos haber definido las restricciones como DEFERRABLE INITIALLY IMMEDIATE
pero luego tendríamos que usar la SET CONSTRAINTS
declaració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 DEFERRABLE
restricciones, una solución es definir las columnas A (bid)
y B (aid)
como NULL
. Los INSERT
procedimientos / declaraciones tendrán que insertarse primero en A
y B
(poner nulos en bid
y aid
respectivamente), luego insertar R
y 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 UPDATE
y DELETE
e 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 KEY
restricciones 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 CASCADE
porque 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
, R
y las claves externas sólo de R a A y B) casi intacta (en realidad, simplificándolo), que añaden otra mesa para A
almacenar 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 R
tabla, 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.
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.
fuente