Зовнішній ключ до кількох таблиць


127

У мене в базі даних 3 відповідні таблиці.

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner int NOT NULL,
    Subject varchar(50) NULL
)

Користувачі належать до декількох груп. Це робиться через багато-багато стосунків, але в цьому випадку не має значення. Квитки можуть бути власниками групи або користувача через поле dbo.Ticket.Owner.

Який би НАЙКРАЩНІший спосіб описати цей зв'язок між квитком та необов'язково користувачем чи групою?

Я думаю, що слід додати прапор до таблиці квитків, який говорить про те, який тип йому належить.


На мій погляд, кожен квиток належить групі. Просто користувач - це група. Який вибір 4 від моделей @ nathan-skerl. Якщо ви використовуєте "Посібники" в якості ключів, все це також працює досить добре
GraemeMiller

Відповіді:


149

У вас є кілька варіантів, всі відрізняються "правильністю" та простотою використання. Як завжди, правильний дизайн залежить від ваших потреб.

  • Ви можете просто створити два стовпці в Ticket, OwnedByUserId та OwnedByGroupId і мати нульові іноземні ключі до кожної таблиці.

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

  • Ви можете створити групу за замовчуванням для кожного користувача та мати квитки, які просто належать справжній Групі або Групі за замовчуванням Користувача.

  • Або (на мій вибір) моделюю організацію, яка виступає базою як для Користувачів, так і для груп і має квитки, що належать цій організації.

Ось приблизний приклад з використанням розміщеної схеми:

create table dbo.PartyType
(   
    PartyTypeId tinyint primary key,
    PartyTypeName varchar(10)
)

insert into dbo.PartyType
    values(1, 'User'), (2, 'Group');


create table dbo.Party
(
    PartyId int identity(1,1) primary key,
    PartyTypeId tinyint references dbo.PartyType(PartyTypeId),
    unique (PartyId, PartyTypeId)
)

CREATE TABLE dbo.[Group]
(
    ID int primary key,
    Name varchar(50) NOT NULL,
    PartyTypeId as cast(2 as tinyint) persisted,
    foreign key (ID, PartyTypeId) references Party(PartyId, PartyTypeID)
)  

CREATE TABLE dbo.[User]
(
    ID int primary key,
    Name varchar(50) NOT NULL,
    PartyTypeId as cast(1 as tinyint) persisted,
    foreign key (ID, PartyTypeId) references Party(PartyID, PartyTypeID)
)

CREATE TABLE dbo.Ticket
(
    ID int primary key,
    [Owner] int NOT NULL references dbo.Party(PartyId),
    [Subject] varchar(50) NULL
)

7
Як виглядає запит на квитки для користувачів / груп? Дякую.
paulkon

4
Яка користь від збережених обчислених стовпців у групах та таблицях користувачів? Первинний ключ у таблиці Party вже гарантує, що в ідентифікаторах групи та ідентифікаторах користувачів не буде перекриття, тому зовнішній ключ повинен бути лише на PartyId. Будь-які написані запити все одно повинні знати таблиці з PartyTypeName у будь-якому разі.
Арін Тейлор

1
@ArinTaylor стовпчик, що зберігається, не дозволяє нам створити користувача типу Party, а також пов'язати його із записом у dbo.Group.
Натан Скерл

3
@paulkon Я знаю, що це давнє запитання, але запит був би таким як. SELECT t.Subject AS ticketSubject, CASE WHEN u.Name IS NOT NULL THEN u.Name ELSE g.Name END AS ticketOwnerName FROM Ticket t INNER JOIN Party p ON t.Owner=p.PartyId LEFT OUTER JOIN User u ON u.ID=p.PartyId LEFT OUTER JOIN Group g on g.ID=p.PartyID;У результаті ви маєте кожну тему квитка та ім'я власника.
Corey McMahon

2
Щодо варіанту 4, чи може хтось підтвердити, чи це анти-шаблон чи рішення анти-шаблону?
інкка

31

Перший варіант у списку @Nathan Skerl - це те, що було реалізовано в проекті, з яким я колись працював, де аналогічні відносини були встановлені між трьома таблицями. (Один із них посилався на двох інших, по одному.)

Отже, таблиця посилань мала два стовпчики із зовнішніми ключами, а також вона мала обмеження гарантувати, що саме одна таблиця (не обидві, ані обидві) не посилається одним рядком.

Ось як це могло виглядати при застосуванні до ваших таблиць:

CREATE TABLE dbo.[Group]
(
    ID int NOT NULL CONSTRAINT PK_Group PRIMARY KEY,
    Name varchar(50) NOT NULL
);

CREATE TABLE dbo.[User]
(
    ID int NOT NULL CONSTRAINT PK_User PRIMARY KEY,
    Name varchar(50) NOT NULL
);

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL CONSTRAINT PK_Ticket PRIMARY KEY,
    OwnerGroup int NULL
      CONSTRAINT FK_Ticket_Group FOREIGN KEY REFERENCES dbo.[Group] (ID),
    OwnerUser int NULL
      CONSTRAINT FK_Ticket_User  FOREIGN KEY REFERENCES dbo.[User]  (ID),
    Subject varchar(50) NULL,
    CONSTRAINT CK_Ticket_GroupUser CHECK (
      CASE WHEN OwnerGroup IS NULL THEN 0 ELSE 1 END +
      CASE WHEN OwnerUser  IS NULL THEN 0 ELSE 1 END = 1
    )
);

Як бачите, у Ticketтаблиці є два стовпці, OwnerGroupпричому OwnerUserобидва вони є нульовими сторонніми ключами. (Відповідні стовпці в двох інших таблицях складаються відповідно первинними ключами.) Обмеження CK_Ticket_GroupUserперевірки гарантує, що лише один з двох стовпців зовнішнього ключа містить посилання (інший - NULL, тому обидва повинні бути зведені нанівець).

(Первинний ключ Ticket.IDдля цього не потрібен для цієї конкретної реалізації, але, безумовно, не зашкодить мати такий у таблиці, як ця.)


1
Це також є в нашому програмному забезпеченні, і я б уникнув, якщо ви намагаєтесь створити загальну рамку доступу до даних. Цей дизайн збільшить складність у шарі програми.
Frank.Germain

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

@Shadoninja: Ви не помиляєтесь. Насправді, я думаю, що це цілком справедливий спосіб її викладати. Я, як правило, добре з таким рішенням, де це виправдано, але це, безумовно, було б не в першу чергу при розгляді варіантів - саме через причину, яку ви накреслили.
Андрій М

2
@ Frank.Germain У цьому випадку ви можете використовувати унікальний зовнішній ключ на основі двох стовпців RefID, RefTypeде RefTypeє фіксований ідентифікатор цільової таблиці. Якщо вам потрібна цілісність, ви можете робити перевірки в тригерному або додатковому шарах. У цьому випадку можливе загальне пошуку. SQL повинен давати визначення FK таким чином, полегшуючи наше життя.
djmj

2

Ще один варіант полягає в тому, щоб в Ticketодному стовпчику було вказано тип власності сутності ( Userабо Group), другий стовпець із посиланням Userабо Groupідентифікатором і НЕ використовувати іноземні ключі, а замість цього покладатися на тригер для забезпечення референтної цілісності.

Тут я бачу дві переваги над чудовою моделлю Натана (нагорі):

  • Більш безпосередня ясність і простота.
  • Простіші запити для запису.

1
Але це не дозволить зробити зовнішній ключ правильно? Я все ще намагаюся з’ясувати правильний дизайн для мого поточного проекту, де одна таблиця може посилатися на принаймні 3, можливо, більше в майбутньому
Може Рау

2

Інший підхід полягає у створенні таблиці асоціацій, яка містить стовпці для кожного потенційного типу ресурсів. У вашому прикладі кожен із двох існуючих типів власників має свою власну таблицю (це означає, що вам є на що посилатися). Якщо це завжди так, ви можете мати щось подібне:

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner_ID int NOT NULL,
    Subject varchar(50) NULL
)

CREATE TABLE dbo.Owner
(
    ID int NOT NULL,
    User_ID int NULL,
    Group_ID int NULL,
    {{AdditionalEntity_ID}} int NOT NULL
)

За допомогою цього рішення ви продовжуватимете додавати нові стовпці, додаючи нові бази до бази даних, і ви будете видаляти та відтворювати шаблон обмеження іноземного ключа, показаний @Nathan Skerl. Це рішення дуже схоже на @Nathan Skerl, але виглядає інакше (залежно від уподобань).

Якщо ви не збираєтеся мати нову таблицю для кожного нового типу власника, можливо, було б добре включити тип власника замість стовпця з іноземним ключем для кожного потенційного власника:

CREATE TABLE dbo.Group
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)  

CREATE TABLE dbo.User
(
    ID int NOT NULL,
    Name varchar(50) NOT NULL
)

CREATE TABLE dbo.Ticket
(
    ID int NOT NULL,
    Owner_ID int NOT NULL,
    Owner_Type string NOT NULL, -- In our example, this would be "User" or "Group"
    Subject varchar(50) NULL
)

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

Вибачте, якщо я помилився з SQL, я просто зібрав це разом.


-4
CREATE TABLE dbo.OwnerType
(
    ID int NOT NULL,
    Name varchar(50) NULL
)

insert into OwnerType (Name) values ('User');
insert into OwnerType (Name) values ('Group');

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

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