¿Es aceptable tener referencias de claves foráneas circulares \ ¿Cómo evitarlas?


29

¿Es aceptable tener una referencia circular entre dos tablas en el campo de clave externa?

Si no, ¿cómo se pueden evitar estas situaciones?

Si es así, ¿cómo se pueden insertar los datos?

A continuación se muestra un ejemplo de dónde (en mi opinión) una referencia circular sería aceptable:

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
    AccountID INT FOREIGN KEY REFERENCES Account(ID)
)

ALTER TABLE Account ADD PrimaryContactID INT FOREIGN KEY REFERENCES Contact(ID)

2
" Si es así, cómo se pueden insertar los datos " - depende del DBMS que se esté utilizando. Postgres, Oracle, SQLite y Apache Derby, por ejemplo, permiten restricciones diferibles que lo harían posible. Con otros DBMS no tiene suerte (pero en primer lugar, todavía cuestionaría la necesidad de tal restricción)
a_horse_with_no_name

Respuestas:


12

Dado que está utilizando campos anulables para las claves foráneas, de hecho, puede construir un sistema que funcione correctamente de la manera que lo imagina. Para insertar filas en la tabla Cuentas, debe tener una fila presente en la tabla Contactos a menos que permita inserciones en Cuentas con un PrimaryContactID nulo. Para crear una fila de contactos sin tener una fila de Cuenta presente, debe permitir que la columna AccountID en la tabla Contactos sea anulable. Esto permite que las cuentas no tengan contactos y permite que los contactos no tengan cuenta. Quizás esto sea deseable, quizás no.

Dicho esto, mi preferencia personal sería tener la siguiente configuración:

CREATE TABLE dbo.Accounts
(
    AccountID INT NOT NULL
        CONSTRAINT PK_Accounts
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , AccountName VARCHAR(255)
);

CREATE TABLE dbo.Contacts
(
    ContactID INT NOT NULL
        CONSTRAINT PK_Contacts
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , ContactName VARCHAR(255)
);

CREATE TABLE dbo.AccountsContactsXRef
(
    AccountsContactsXRefID INT NOT NULL
        CONSTRAINT PK_AccountsContactsXRef
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , AccountID INT NOT NULL
        CONSTRAINT FK_AccountsContactsXRef_AccountID
        FOREIGN KEY REFERENCES dbo.Accounts(AccountID)
    , ContactID INT NOT NULL
        CONSTRAINT FK_AccountsContactsXRef_ContactID
        FOREIGN KEY REFERENCES dbo.Contacts(ContactID)
    , IsPrimary BIT NOT NULL 
        CONSTRAINT DF_AccountsContactsXRef
        DEFAULT ((0))
    , CONSTRAINT UQ_AccountsContactsXRef_AccountIDContactID
        UNIQUE (AccountID, ContactID)
);

CREATE UNIQUE INDEX IX_AccountsContactsXRef_Primary
ON dbo.AccountsContactsXRef(AccountID, IsPrimary)
WHERE IsPrimary = 1;

Esto proporciona la capacidad de:

  1. Delinee claramente las relaciones entre contactos y cuentas a través de una tabla de referencias cruzadas de la manera que Pieter recomienda en su respuesta
  2. Mantener integridad referencial de una manera sólida, no circular.
  3. Proporcione una lista altamente mantenible de contactos principales a través del IX_AccountsContactsXRef_Primaryíndice. Este índice contiene un filtro, por lo que solo funcionará en plataformas que los admitan. Dado que este índice se especifica con la UNIQUEopción, solo puede haber un único contacto principal para cada cuenta.

Por ejemplo, si desea mostrar una lista de todos los contactos, con una columna que indica el estado "principal", que muestra los contactos principales en la parte superior de la lista para cada cuenta, puede hacer lo siguiente:

SELECT A.AccountName
    , C.ContactName
    , XR.IsPrimary
FROM dbo.Accounts A
    INNER JOIN dbo.AccountsContactsXRef XR ON A.AccountID = XR.AccountID
    INNER JOIN dbo.Contacts C ON XR.ContactID = C.ContactID
ORDER BY A.AccountName
    , XR.IsPrimary DESC
    , C.ContactName;

El índice filtrado evita la inserción de más de un contacto principal por cuenta, al tiempo que proporciona un método rápido para devolver una lista de contactos principales. Uno podría imaginar fácilmente otra columna, IsActivecon un índice filtrado no único para mantener un historial de contactos por cuenta, incluso después de que ese contacto ya no esté asociado con la cuenta:

ALTER TABLE dbo.AccountsContactsXRef
ADD IsActive BIT NOT NULL
CONSTRAINT DF_AccountsContactsXRef_IsActive
DEFAULT ((1));

CREATE INDEX IX_AccountsContactsXRef_IsActive
ON dbo.AccountsContactsXRef(IsActive)
WHERE IsActive = 1;

1
¿Diría, en general, que deben evitarse las referencias circulares? Soy de la opinión de que no son malos y los he usado para lograr diseños efectivos. Hacen que las eliminaciones sean un poco más complicadas, ya que requieren y actualizan a NULL en la entidad que de otro modo sería la única matriz, pero creo que es un precio bajo para pagar por la conveniencia. Los uso en Postgres, donde el campo FK es anulable, así que creo una fila con NULL y luego actualizo el campo FK a la PK desde la tabla secundaria para lograr prácticamente la misma función que se describe en el OP
anfibio

No me gustan las referencias circulares simplemente porque tienden a complicar innecesariamente el diseño, y la mayoría de las veces no ofrecen ningún beneficio de rendimiento significativo que valga la pena. Soy fanático de la Navaja de Occam, y como resultado tienden a la solución más simple para un problema dado.
Max Vernon

1
Estoy totalmente de acuerdo con la navaja de afeitar de Occam, pero el diseño descrito anteriormente me permitió evitar algunas consultas o uniones secundarias sin violar necesariamente la tercera forma normal. Agradezco sus comentarios
anfibio

6

No, no es aceptable tener referencias de claves foráneas circulares. No solo porque sería imposible insertar datos sin eliminar y recrear constantemente la restricción. pero porque es un modelo fundamentalmente defectuoso de todos y cada uno de los dominios que se me ocurren. En su ejemplo, no puedo pensar en ningún dominio en el que la relación entre la Cuenta y el Contacto no sea NN, que requiera una tabla de unión con referencias FK a la Cuenta y al Contacto.

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
)

CREATE TABLE AccountContact
(
    AccountID INT FOREIGN KEY REFERENCES Account(ID),
    ContactID INT FOREIGN KEY REFERENCES Contact(ID),

    primary key(AccountID,ContactID)
)

55
" sería imposible insertar datos " - no, no sería imposible. Simplemente declare las restricciones como diferibles. Pero sí estoy de acuerdo: en casi todos los casos, las referencias circulares son un mal diseño.
a_horse_with_no_name

3
@a_horse: no es posible definir una referencia diferible en SQL Server ... Sé que puede hacerlo en Oracle, solo quería señalar la discrepancia.
Max Vernon

2
@MaxVernon: la pregunta no es solo acerca de SQL Server y hay más DBMS que solo Oracle que admiten restricciones diferibles, pero como dije: estoy de acuerdo con Pieter en que el diseño en sí es incorrecto (y su solución tiene mucho más sentido)
a_horse_with_no_name

44
Dejando a un lado los detalles de cualquier ejemplo, en términos generales no hay nada necesariamente incorrecto o "defectuoso" sobre tener restricciones de integridad referencial recíprocas (es decir, "circulares"). Esto es en efecto solo un ejemplo de una dependencia de unión. Las dependencias de unión son buenas en principio si su DBMS le permite implementarlas. Es solo que en los DBMS de SQL no es muy fácil implementar dependencias complejas entre tablas.
nvogel

66
@Pieter, 1-1 no es el único ejemplo de dependencia de unión, y ni siquiera es un caso particularmente especial. Hay casos en los que las restricciones de dependencia de unión tienen mucho sentido.
nvogel

1

Puede hacer que su objeto externo apunte al contacto principal, en lugar de a la cuenta. Sus datos se verían así:

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
    AccountID INT FOREIGN KEY REFERENCES Account(ID)
)

CREATE TABLE AccountOwner (
    Other Stuff Here . . .
    PrimaryContactID INT FOREIGN KEY REFERENCES Contact(ID)
)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.