Внешний ключ для нескольких таблиц

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.

Каким будет НАИБОЛЕЕ ПРАВИЛЬНЫЙ способ описания отношений между заявкой и, возможно, пользователем или группой?

Я думаю, что мне следует добавить в таблицу билетов флаг, который говорит, какой тип владеет им.

Darthg8r
источник
На мой взгляд, каждый билет принадлежит группе. Просто пользователь - это группа из одного человека. Какой выбор 4 из моделей @ nathan-skerl. Если вы используете Guids в качестве ключей, все также работает довольно хорошо
GraemeMiller

Ответы:

149

У вас есть несколько вариантов, все они различаются по «правильности» и простоте использования. Как всегда, правильный дизайн зависит от ваших потребностей.

  • Вы можете просто создать два столбца в Ticket, OwnedByUserId и OwnedByGroupId, и иметь внешние ключи, допускающие значение NULL, для каждой таблицы.

  • Вы можете создать справочные таблицы 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
Как будет выглядеть запрос на билеты пользователя / группы? Спасибо.
Паулкон
4
В чем преимущество постоянных вычисляемых столбцов в таблицах групп и пользователей? Первичный ключ в таблице Party уже гарантирует, что не будет перекрытия идентификаторов групп и пользователей, поэтому внешний ключ должен быть только в PartyId. Любые написанные запросы все равно должны знать таблицы из PartyTypeName.
Арин Тейлор
1
@ArinTaylor: постоянный столбец не позволяет нам создать партию типа User и связать ее с записью в 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;В результате у вас будут все темы и имена владельцев билетов.
Кори МакМахон,
2
Что касается варианта 4, может ли кто-нибудь подтвердить, является ли это антипаттерном или решением для антипаттерна?
inckka
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, оба из которых являются внешними ключами, допускающими значение NULL. (Соответствующие столбцы в двух других таблицах соответственно становятся первичными ключами.) Контрольное CK_Ticket_GroupUserограничение гарантирует, что только один из двух столбцов внешнего ключа содержит ссылку (другой имеет значение NULL, поэтому оба должны допускать значение NULL).

(Первичный ключ Ticket.IDне требуется для этой конкретной реализации, но, безусловно, не повредит его наличие в такой таблице.)

Андрей М
источник
1
Это то же самое, что и в нашем программном обеспечении, и я бы избегал, если вы пытаетесь создать общую структуру доступа к данным. Такой дизайн повысит сложность уровня приложения.
Frank.Germain
4
Я действительно новичок в SQL, поэтому поправьте меня, если это не так, но этот дизайн кажется подходящим для использования, когда вы очень уверены, что вам понадобятся только два типа владельца билета. В дальнейшем, если будет введен третий тип владельца билета, вам придется добавить в таблицу третий столбец внешнего ключа, допускающий значение NULL.
Шадонинджа 01
@Shadoninja: Вы не ошиблись. На самом деле, я думаю, что это совершенно справедливо. В целом меня устраивает такое решение, если оно оправдано, но я определенно не думаю об этом в первую очередь при рассмотрении вариантов - именно по той причине, которую вы указали.
Андрей М
2
@ Frank.Germain В этом случае вы можете использовать уникальный внешний ключ на основе двух столбцов RefID, RefTypeгде RefType- фиксированный идентификатор целевой таблицы. Если вам нужна целостность, вы можете выполнять проверки на уровне триггера или приложения. В этом случае возможен общий поиск. SQL должен позволять такое определение FK, что облегчает нашу жизнь.
djmj
2

Еще один вариант состоит в том, чтобы иметь в Ticketодном столбце, определяющем тип объекта-владельца ( Userили Group), второй столбец со ссылкой Userили Groupидентификатором и НЕ использовать внешние ключи, а вместо этого полагаться на триггер для обеспечения ссылочной целостности.

Я вижу здесь два преимущества перед превосходной моделью Натана (см. Выше):

  • Более непосредственная ясность и простота.
  • Более простые запросы для написания.
Ян Ланковский
источник
1
Но это не позволяет использовать внешний ключ, верно? Я все еще пытаюсь придумать правильный дизайн для моего текущего проекта, где одна таблица может ссылаться как минимум на 3, а может и больше в будущем
Can Rau
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, но выглядит иначе (в зависимости от предпочтений).

Если у вас не будет новой таблицы для каждого нового типа владельца, то, возможно, было бы хорошо включить owner_type вместо столбца внешнего ключа для каждого потенциального владельца:

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, я просто собрал это вместе.

smoosh911
источник
-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');

Я думаю, что это был бы наиболее общий способ представить то, что вы хотите, вместо использования флага.

Франсиско Сото
источник