web-dev-qa-db-fra.com

Est-il acceptable d'avoir des références de clés étrangères circulaires \ Comment les éviter?

Est-il acceptable d'avoir une référence circulaire entre deux tables sur le champ de clé étrangère?

Sinon, comment éviter ces situations?

Si oui, comment insérer des données?

Voici un exemple où (à mon avis) une référence circulaire serait acceptable:

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)
29
KidCode

Puisque vous utilisez des champs nullables pour les clés étrangères, vous pouvez en fait construire un système qui fonctionne correctement comme vous l'imaginez. Afin d'insérer des lignes dans la table des comptes, vous devez avoir une ligne présente dans la table des contacts, sauf si vous autorisez les insertions dans les comptes avec un PrimaryContactID nul. Afin de créer une ligne de contact sans avoir déjà une ligne de compte présente, vous devez autoriser la colonne AccountID dans la table Contacts pour être nullable. Cela permet aux comptes de ne pas avoir de contacts et aux contacts de ne pas avoir de compte. C'est peut-être souhaitable, peut-être pas.

Cela dit, ma préférence personnelle serait d'avoir la configuration suivante:

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;

Cela permet de:

  1. Délimitez clairement les relations entre les contacts et les comptes grâce à une table de correspondance, comme le recommande Pieter dans sa réponse
  2. Maintenir l'intégrité référentielle d'une manière saine et non circulaire.
  3. Fournir une liste hautement maintenable de contacts principaux via le IX_AccountsContactsXRef_Primary index. Cet index contient un filtre, il ne fonctionnera donc que sur les plateformes qui les prennent en charge. Étant donné que cet index est spécifié avec l'option UNIQUE, il ne peut y avoir qu'un seul contact principal pour chaque compte.

Par exemple, si vous souhaitez afficher une liste de tous les contacts, avec une colonne indiquant le statut "principal", montrant les contacts principaux en haut de la liste pour chaque compte, vous pouvez faire:

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;

L'index filtré empêche l'insertion de plus d'un seul contact principal par compte, tout en fournissant simultanément une méthode rapide de renvoi d'une liste de contacts principaux. On pourrait facilement imaginer une autre colonne, IsActive avec un index filtré non unique pour conserver un historique des contacts par compte, même après que ce contact ne soit plus associé au compte:

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;
12
Max Vernon

Non, il n'est pas acceptable d'avoir des références de clés étrangères circulaires. Non seulement parce qu'il serait impossible d'insérer des données sans supprimer et recréer constamment la contrainte. mais parce que c'est un modèle fondamentalement imparfait de tous les domaines auxquels je peux penser. Dans votre exemple, je ne peux penser à aucun domaine dans lequel la relation entre le compte et le contact n'est pas N-N, nécessitant une table de jonction avec des références FK vers le compte et le contact.

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)
)
5
Pieter Geerkens

Vous pouvez faire pointer votre objet externe vers le contact principal plutôt que vers le compte. Vos données ressembleraient à ceci:

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)
)
1
William Jockusch