Ist es akzeptabel, zirkuläre Fremdschlüsselverweise zu haben \ Wie vermeide ich sie?

29

Ist es akzeptabel, einen Zirkelverweis zwischen zwei Tabellen im Fremdschlüsselfeld zu haben?

Wenn nicht, wie können diese Situationen vermieden werden?

Wenn ja, wie können Daten eingefügt werden?

Nachfolgend finden Sie ein Beispiel dafür, wo (meiner Meinung nach) ein Zirkelverweis akzeptabel wäre:

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
quelle
2
" Wenn ja, wie können Daten eingefügt werden? " - hängt vom verwendeten DBMS ab. Beispielsweise erlauben Postgres, Oracle, SQLite und Apache Derby aufschiebbare Einschränkungen, die dies ermöglichen würden. Mit anderen DBMS haben Sie Pech (aber ich würde die Notwendigkeit einer solchen Einschränkung immer noch bestreiten)
a_horse_with_no_name

Antworten:

12

Da Sie nullfähige Felder für die Fremdschlüssel verwenden, können Sie tatsächlich ein System erstellen, das so funktioniert, wie Sie es sich vorstellen. Um Zeilen in die Accounts-Tabelle einzufügen, muss in der Contacts-Tabelle eine Zeile vorhanden sein, sofern Sie keine Einfügungen in Accounts mit einer PrimaryContactID von Null zulassen. Um eine Kontaktzeile zu erstellen, ohne dass bereits eine Kontozeile vorhanden ist, müssen Sie zulassen, dass die Spalte "AccountID" in der Tabelle "Kontakte" nullwertfähig ist. Auf diese Weise können Konten keine Kontakte und Kontakte kein Konto haben. Vielleicht ist das wünschenswert, vielleicht auch nicht.

Persönlich würde ich jedoch folgendes Setup bevorzugen:

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;

Dies bietet die Möglichkeit:

  1. Zeichnen Sie die Beziehungen zwischen Kontakten und Konten anhand einer Querverweistabelle klar ab, wie es Pieter in seiner Antwort empfiehlt
  2. Bewahren Sie die referenzielle Integrität auf eine gesunde, nicht kreisförmige Weise auf.
  3. Stellen Sie über den Index eine hochgradig verwaltbare Liste der primären KontakteIX_AccountsContactsXRef_Primary bereit. Dieser Index enthält einen Filter, sodass er nur auf Plattformen funktioniert, die diesen unterstützen. Da dieser Index mit der UNIQUEOption angegeben wird, kann es für jedes Konto immer nur einen einzigen Hauptkontakt geben.

Wenn Sie beispielsweise eine Liste aller Kontakte anzeigen möchten, wobei eine Spalte den "primären" Status anzeigt und die primären Kontakte für jedes Konto oben in der Liste angezeigt werden, können Sie Folgendes tun:

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;

Der gefilterte Index verhindert das Einfügen von mehr als einem primären Kontakt pro Konto und bietet gleichzeitig eine schnelle Methode zum Zurückgeben einer Liste primärer Kontakte. Man kann sich leicht eine andere Spalte IsActivemit einem nicht eindeutigen gefilterten Index vorstellen , um den Verlauf der Kontakte pro Konto aufrechtzuerhalten, auch wenn dieser Kontakt nicht mehr mit dem Konto verknüpft ist:

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
quelle
1
Würden Sie allgemein sagen, dass Zirkelverweise vermieden werden sollten? Ich bin der Meinung, dass sie nicht schlecht sind und sie verwendet haben, um effektive Designs zu erzielen. Sie machen Löschvorgänge insofern etwas komplizierter, als sie NULL erfordern und in der ansonsten ausschließlich übergeordneten Entität auf NULL aktualisieren. Ich benutze sie in Postgres, wo das FK-Feld nullbar ist, also erstelle ich eine Zeile mit NULL und aktualisiere dann das FK-Feld in der PK aus der Kindertabelle, um so ziemlich die gleiche Funktion zu erreichen, wie sie im OP
amphibient
Ich mag Zirkelverweise nicht, nur weil sie das Design unnötig komplizieren und die meiste Zeit keinen signifikanten Leistungsvorteil bieten, der sich auszahlt. Ich bin ein Fan von Occam's Razor und tendiere daher zur einfachsten Lösung für ein bestimmtes Problem.
Max Vernon
1
Ich bin alles für Occams Rasiermesser, aber das oben beschriebene Design ermöglichte es mir, einige zweite Anfragen oder Verknüpfungen zu vermeiden, ohne notwendigerweise die dritte Normalform zu verletzen. Ich
freue
6

Nein, zirkuläre Fremdschlüsselreferenzen sind nicht zulässig. Nicht nur, weil es unmöglich wäre, Daten einzufügen, ohne die Einschränkung ständig zu löschen und neu zu erstellen. aber weil es ein grundlegend fehlerhaftes Modell für jeden Bereich ist, den ich mir vorstellen kann. In Ihrem Beispiel fällt mir keine Domäne ein, in der die Beziehung zwischen Konto und Kontakt nicht NN ist, sodass eine Junction-Tabelle mit FK-Verweisen auf Konto und Kontakt erforderlich ist.

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
quelle
5
" es wäre unmöglich, daten einzufügen " - nein, es wäre nicht unmöglich. Deklarieren Sie einfach die Einschränkungen als aufschiebbar. Aber ich stimme dem zu: In fast allen Fällen sind kreisförmige Verweise ein schlechtes Design.
a_horse_with_no_name
3
@a_horse - es ist nicht möglich, einen aufschiebbaren Verweis in SQL Server zu definieren ... Ich weiß, Sie können in Oracle, wollte nur auf die Diskrepanz hinweisen.
Max Vernon
2
@MaxVernon: Die Frage betrifft nicht nur SQL Server und es gibt mehr DBMS als nur Oracle, die aufschiebbare Einschränkungen unterstützen - aber wie gesagt: Ich stimme Pieter zu, dass das Design selbst falsch ist (und seine Lösung viel sinnvoller ist)
a_horse_with_no_name
4
Abgesehen von den Besonderheiten eines Beispiels gibt es im Allgemeinen nichts Falsches oder "Fehlerhaftes" daran, wechselseitige (dh "kreisförmige") referenzielle Integritätsbeschränkungen zu haben. Dies ist nur ein Beispiel für eine Join-Abhängigkeit. Join-Abhängigkeiten sind grundsätzlich eine gute Sache, wenn Sie sie in Ihrem DBMS implementieren können. Es ist nur so, dass es in SQL-DBMS nicht einfach ist, komplexe Abhängigkeiten zwischen Tabellen zu implementieren.
nvogel
6
@Pieter, 1-1 ist nicht das einzige Beispiel für eine Join-Abhängigkeit, und es ist nicht einmal ein besonderer Fall. Es gibt Fälle, in denen Verknüpfungsabhängigkeitsbeschränkungen absolut sinnvoll sind.
nvogel
1

Sie können Ihr externes Objekt auf den primären Kontakt anstatt auf das Konto verweisen lassen. Ihre Daten würden so aussehen:

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
quelle