Допустимо ли иметь круговые ссылки на внешние ключи \ Как их избежать?

29

Допустимо ли иметь круговую ссылку между двумя таблицами в поле внешнего ключа?

Если нет, как можно избежать этих ситуаций?

Если да, то как можно вставить данные?

Ниже приведен пример того, где (по моему мнению) круговая ссылка будет приемлемой:

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
источник
2
« Если так, как данные могут быть вставлены » - зависит от используемой СУБД. Например, Postgres, Oracle, SQLite и Apache Derby допускают отложенные ограничения, которые делают это возможным. С другими СУБД вам не повезло (но я все равно оспаривал бы необходимость такого ограничения в первую очередь)
a_horse_with_no_name

Ответы:

12

Поскольку вы используете пустые поля для внешних ключей, вы можете создать систему, которая будет работать правильно, как вы ее себе представляете. Для вставки строк в таблицу «Учетные записи» необходимо, чтобы в таблице «Контакты» присутствовала строка, если только вы не разрешаете вставки в «Учетные записи» с нулевым PrimaryContactID. Чтобы создать строку контакта, в которой еще нет строки «Учетная запись», необходимо разрешить использование столбца «AccountID» в таблице «Контакты». Это позволяет Учетным записям не иметь контактов, и позволяет Контактам не иметь учетной записи. Возможно это желательно, возможно нет.

Сказав это, мое личное предпочтение будет иметь следующую настройку:

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;

Это дает возможность:

  1. Четко разграничить отношения между контактами и учетными записями через таблицу перекрестных ссылок, как рекомендует Питер в своем ответе.
  2. Поддерживать ссылочную целостность в звуковой, некруговой форме.
  3. Предоставить список основных контактов помощью IX_AccountsContactsXRef_Primaryиндекса. Этот индекс содержит фильтр, поэтому он будет работать только на платформах, которые их поддерживают. Поскольку этот индекс указывается с помощью UNIQUEопции, для каждой учетной записи может быть только один основной контакт.

Например, если вы хотите отобразить список всех контактов с колонкой, обозначающей «основной» статус, с указанием основных контактов в верхней части списка для каждой учетной записи, вы можете сделать следующее:

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;

Отфильтрованный индекс предотвращает вставку более одного основного контакта для каждой учетной записи, одновременно обеспечивая быстрый способ возврата списка основных контактов. Можно легко представить другой столбец IsActiveс неуникальным отфильтрованным индексом для ведения истории контактов для учетной записи, даже после того, как этот контакт больше не связан с учетной записью:

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
Вы бы сказали, что вообще следует избегать циклических ссылок? Я придерживаюсь мнения, что они неплохие и использовали их для создания эффективных замыслов. Они действительно делают удаление немного более сложным в том смысле, что требуют и обновляют до NULL в противном случае, если бы родитель был единственным, но я считаю, что это низкая цена за удобство. Я использую их в Postgres, где поле FK имеет значение NULL, поэтому я создаю строку с ним NULL, а затем обновляю поле FK до PK из дочерней таблицы, чтобы в значительной степени выполнить ту же функцию, что описана в OP
amphibient
Мне не нравятся циклические ссылки просто потому, что они, как правило, излишне усложняют дизайн, и большую часть времени не предлагают какого-либо существенного выигрыша в производительности, которое стоит компромисса. Я фанат бритвы Оккама и в результате стремлюсь к простейшему решению данной проблемы.
Макс Вернон,
1
Я все за бритву Оккама, но описанный выше дизайн позволил мне избежать некоторых 2-х запросов или объединений, при этом необязательно нарушая 3-ю обычную форму. Я ценю ваше мнение
amphibient
6

Нет, недопустимо иметь круговые ссылки на внешние ключи. Не только потому, что было бы невозможно вставить данные без постоянного удаления и воссоздания ограничения. но потому что это принципиально некорректная модель любой и каждой области, о которой я могу думать. В вашем примере я не могу представить ни одного домена, в котором отношения между Учетной записью и Контактом не являются NN, требуя таблицы соединений со ссылками FK обратно на Учетную запись и Контакт.

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
« было бы невозможно вставить данные » - нет, это не было бы невозможно. Просто объявите ограничения как отложенные. Но я согласен: почти во всех случаях круговые ссылки - плохой дизайн.
a_horse_with_no_name
3
@a_horse - невозможно определить отложенную ссылку в SQL Server ... Я знаю, что вы можете в Oracle, просто хотел указать на несоответствие.
Макс Вернон
2
@MaxVernon: вопрос не только в SQL Server, и есть больше СУБД, чем просто Oracle, которые поддерживают отложенные ограничения - но, как я уже сказал: я согласен с Питером, что сам дизайн неправильный (и его решение имеет гораздо больше смысла)
a_horse_with_no_name
4
Оставляя в стороне специфику какого-либо одного примера, в общих чертах нет ничего плохого или «ошибочного» в отношении наличия взаимных (то есть «круговых») ограничений ссылочной целостности. По сути, это всего лишь пример зависимости присоединения. Зависимости соединений - это хорошая вещь в принципе, если ваша СУБД позволяет вам их реализовывать. Просто в СУБД SQL сложно реализовать сложные зависимости между таблицами.
nvogel
6
@Pieter, 1-1 - не единственный пример зависимости соединения, и это даже не особый случай. Есть случаи, когда ограничения зависимостей соединения имеют смысл.
nvogel
1

Ваш внешний объект может указывать на основной контакт, а не на учетную запись. Ваши данные будут выглядеть так:

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)
)
Уильям Джокуш
источник