Чи допустимо мати кругові посилання на іноземні ключі \ Як їх уникнути?


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)

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, а потім оновлюю поле FK до PK з дочірньої таблиці, щоб значною мірою виконати ту саму функцію, що описана в ОП
амфібій

Мені не подобаються циркулярні посилання просто тому, що вони, як правило, ускладнюють дизайн, і більшість часу не пропонують жодної суттєвої вигоди від продуктивності, що вартує компромісу. Я прихильник бритви Оккама, і в результаті прагну до найпростішого вирішення даної проблеми.
Макс Вернон

1
Я все за бритву Occam, але описаний дизайн дозволив мені уникнути якихось 2-х запитів або приєднань, не обов'язково порушуючи 3-ю нормальну форму. Я ціную ваші відгуки
амфібій

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)
)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.