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

29

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)
KidCode
la source
2
" Si oui, comment insérer des données " - dépend du SGBD utilisé. Postgres, Oracle, SQLite et Apache Derby par exemple autorisent des contraintes différées qui rendraient cela possible. Avec d'autres SGBD, vous n'avez pas de chance (mais je contesterais tout de même la nécessité d'une telle contrainte en premier lieu)
a_horse_with_no_name

Réponses:

12

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 de manière saine et non circulaire.
  3. Fournissez une liste hautement maintenable de contacts principaux via l' IX_AccountsContactsXRef_Primaryindex. Cet index contient un filtre, il ne fonctionnera donc que sur les plates-formes qui les prennent en charge. Étant donné que cet index est spécifié avec l' UNIQUEoption, 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, IsActiveavec 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;
Max Vernon
la source
1
diriez-vous, en général, que les références circulaires devraient être évitées? Je suis d'avis qu'ils ne sont pas mauvais et les ai utilisés pour réaliser des conceptions efficaces. Ils rendent les suppressions un peu plus compliquées dans la mesure où ils nécessitent et sont mis à jour vers NULL dans l'entité par ailleurs qui ne serait que parent, mais je trouve que c'est un prix bas à payer pour la commodité. Je les utilise dans Postgres, où le champ FK est nullable, donc je crée une ligne avec lui NULL et puis met à jour le champ FK vers le PK à partir de la table enfant pour accomplir à peu près la même fonction que celle décrite dans l'OP
amphibient
Je n'aime pas les références circulaires simplement parce qu'elles ont tendance à compliquer inutilement la conception, et la plupart du temps n'offrent aucun avantage significatif en termes de performances qui mérite le compromis. Je suis un fan du rasoir d'Occam et, par conséquent, je tend vers la solution la plus simple pour un problème donné.
Max Vernon
1
Je suis tout à fait pour le rasoir d'Occam mais la conception décrite ci-dessus m'a permis d'éviter certaines 2e requêtes ou jointures tout en ne violant pas nécessairement la 3e forme normale. J'apprécie vos commentaires
amphibient
6

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 NN, 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)
)
Pieter Geerkens
la source
5
" il serait impossible d'insérer des données " - non, ce ne serait pas impossible. Déclarez simplement les contraintes comme reportables. Mais je suis d'accord: dans presque tous les cas, les références circulaires sont une mauvaise conception.
a_horse_with_no_name
3
@a_horse - il n'est pas possible de définir une référence déférable dans SQL Server ... Je sais que vous pouvez dans Oracle, je voulais juste souligner la différence.
Max Vernon
2
@MaxVernon: la question ne concerne pas seulement SQL Server et il y a plus de SGBD qu'Oracle qui prennent en charge les contraintes reportables - mais comme je l'ai dit: je suis d'accord avec Pieter que la conception elle-même est mauvaise (et sa solution est beaucoup plus logique)
a_horse_with_no_name
4
Laissant de côté les spécificités d'un seul exemple, en termes généraux, il n'y a rien de nécessairement erroné ou "défectueux" à avoir des contraintes d'intégrité référentielle réciproques (c'est-à-dire "circulaires"). Il s'agit en fait d'un exemple de dépendance de jointure. Les dépendances de jointure sont une bonne chose en principe si votre SGBD vous permet de les implémenter. C'est juste que dans les SGBD SQL, il n'est pas très facile d'implémenter des dépendances complexes entre les tables.
nvogel
6
@Pieter, 1-1 n'est pas le seul exemple de dépendance de jointure, et ce n'est même pas un cas particulièrement spécial. Il existe des cas où les contraintes de dépendance de jointure sont parfaitement logiques.
nvogel
1

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)
)
William Jockusch
la source