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:
- 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
- Mantener integridad referencial de una manera sólida, no circular.
- 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 UNIQUE
opció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, IsActive
con 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;