È accettabile avere riferimenti a chiave esterna circolare \ Come evitarli?


29

È accettabile avere un riferimento circolare tra due tabelle nel campo chiave esterna?

In caso contrario, come possono essere evitate queste situazioni?

In tal caso, come possono essere inseriti i dati?

Di seguito è riportato un esempio di dove (a mio avviso) un riferimento circolare sarebbe accettabile:

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
" In tal caso, come possono essere inseriti i dati " - dipende dal DBMS in uso. Postgres, Oracle, SQLite e Apache Derby, ad esempio, consentono vincoli differibili che lo renderebbero possibile. Con altri DBMS sei sfortunato (Ma vorrei ancora contestare la necessità di un tale vincolo in primo luogo)
a_horse_with_no_name

Risposte:


12

Dato che stai usando campi nullable per le chiavi esterne, puoi infatti costruire un sistema che funzioni correttamente come lo vedi. Per inserire righe nella tabella Account è necessario disporre di una riga presente nella tabella Contatti a meno che non si consentano inserimenti in Account con un PrimaryContactID nullo. Per creare una riga di contatto senza disporre già di una riga Account, è necessario rendere nulla la colonna AccountID nella tabella Contatti. Ciò consente agli account di non avere contatti e ai contatti di non avere account. Forse questo è desiderabile, forse no.

Detto questo, la mia preferenza personale sarebbe quella di avere la seguente configurazione:

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;

Ciò consente di:

  1. Delinea chiaramente le relazioni tra contatti e account attraverso una tabella di riferimenti incrociati come Pieter raccomanda nella sua risposta
  2. Mantenere l'integrità referenziale in modo solido e non circolare.
  3. Fornire un elenco altamente gestibile di contatti primari tramite l' IX_AccountsContactsXRef_Primaryindice. Questo indice contiene un filtro, quindi funzionerà solo su piattaforme che li supportano. Poiché questo indice è specificato con l' UNIQUEopzione, può esserci un solo contatto primario per ogni account.

Ad esempio, se si desidera visualizzare un elenco di tutti i contatti, con una colonna che indica lo stato "principale", che mostra i contatti primari nella parte superiore dell'elenco per ciascun account, è possibile:

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'indice filtrato impedisce l'inserimento di più di un singolo contatto principale per account, fornendo contemporaneamente un metodo rapido per restituire un elenco di contatti primari. Si potrebbe facilmente immaginare un'altra colonna, IsActivecon un indice filtrato non univoco per mantenere una cronologia dei contatti per account, anche dopo che quel contatto non è più associato all'account:

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
diresti, in generale, che i riferimenti circolari dovrebbero essere evitati? Sono dell'opinione che non siano cattivi e li abbiano usati per realizzare progetti efficaci. Rendono le cancellazioni leggermente più complicate in quanto richiedono e si aggiornano su NULL nell'entità genitore che sarebbe altrimenti unica, ma trovo che sia un prezzo basso da pagare per comodità. Li uso in Postgres, dove il campo FK è nullable quindi creo una riga con esso NULL e quindi aggiorno il campo FK al PK dalla tabella figlio per svolgere praticamente la stessa funzione descritta nell'OP
anfibio il

Non mi piacciono i riferimenti circolari semplicemente perché tendono a complicare inutilmente il design e la maggior parte delle volte non offrono alcun vantaggio significativo in termini di prestazioni degno del compromesso. Sono un fan di Occam's Razor e, di conseguenza, tendo alla soluzione più semplice per un determinato problema.
Max Vernon,

1
Sono tutto per il rasoio di Occam, ma il design sopra descritto mi ha permesso di evitare alcune seconde domande o unioni, senza necessariamente violare la terza forma normale. Apprezzo il tuo feedback
anfibio,

6

No, non è accettabile avere riferimenti a chiave esterna circolare. Non solo perché sarebbe impossibile inserire dati senza eliminare e ricreare costantemente il vincolo. ma perché è un modello fondamentalmente imperfetto di ogni dominio a cui riesco a pensare. Nel tuo esempio non riesco a pensare a nessun dominio in cui la relazione tra Account e Contact non sia NN, che richiede una tabella di giunzione con riferimenti FK sia su Account sia su 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
" sarebbe impossibile inserire dati " - no, non sarebbe impossibile. Dichiara semplicemente i vincoli come differibili. Ma sono d'accordo: in quasi tutti i casi i riferimenti circolari sono un cattivo design.
a_horse_with_no_name

3
@a_horse - non è possibile definire un riferimento differibile in SQL Server ... So che puoi farlo in Oracle, volevo solo sottolineare la discrepanza.
Max Vernon,

2
@MaxVernon: la domanda non riguarda solo SQL Server e ci sono più DBMS che solo Oracle che supportano vincoli differibili - ma come ho detto: concordo con Pieter che il design stesso è sbagliato (e la sua soluzione ha molto più senso)
a_horse_with_no_name

4
Tralasciando le specificità di ogni singolo esempio, in termini generali non c'è nulla di necessariamente sbagliato o "imperfetto" nell'avere vincoli di integrità referenziale reciproca (cioè "circolare"). Questo è in effetti solo un esempio di dipendenza dipendente. Le dipendenze di join sono una buona cosa in linea di principio se il tuo DBMS ti consente di implementarle. È solo che nei DBMS SQL non è molto semplice implementare dipendenze complesse tra le tabelle.
nvogel,

6
@Pieter, 1-1 non è l'unico esempio di dipendenza di join e non è nemmeno un caso particolarmente speciale. Ci sono casi in cui i vincoli di dipendenza dai join hanno perfettamente senso.
nvogel,

1

È possibile che l'oggetto esterno punti al contatto principale, piuttosto che all'account. I tuoi dati sarebbero così:

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)
)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.