多个表的外键


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字段拥有。

最正确的方式是什么描述票证与用户或组之间的这种关系?

我在想我应该在票证表中添加一个标志,说明该标志的类型。


在我看来,每张票都是一个团体拥有的。只是一个用户是一组。@ nathan-skerl模型中的哪个选择4。如果您使用的GUID作为键,整个事情也工作得很好
GraemeMiller

Answers:


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 2015年

4
组和用户表中的持久化计算列有什么好处?Party表中的主键已经确保了组ID和用户ID中不会有重叠,因此外键只需要单独位于PartyId上。无论如何,任何编写的查询仍然需要从PartyTypeName知道表。
阿林·泰勒

1
@ArinTaylor保留的列使我们无法创建类型为User的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,有人可以确认这是反模式还是反模式的解决方案?
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表有两列OwnerGroupOwnerUser,这两列都是可为空的外键。(其他两个表中的相应列相应地成为主键。)CK_Ticket_GroupUser检查约束确保两个外键列中只有一个包含引用(另一个为NULL,这就是为什么两个都必须为可空的原因)。

Ticket.ID对于这个特定的实现,主键on 并不是必需的,但是在这样的表中拥有一个键绝对不会造成损害。)


1
这也是我们软件中的功能,如果您尝试创建通用数据访问框架,我将避免使用。这种设计将增加应用程序层的复杂性。
Frank.Germain

4
我对SQL真的很陌生,所以如果这是错误的,请纠正我,但是当您非常有信心只需要两种所有者类型的票证时,这种设计似乎是一种使用方法。如果引入了第三种票证所有者类型,那么您将不得不在表中添加第三个可空外键列。
Shadoninja'1

@Shadoninja:你没看错。实际上,我认为这是一种完全公平的说法。对于这种解决方案,我通常会认为是可以的,但是考虑到选择的理由时,我当然不会首先想到它-正是由于您概述了原因。
Andriy M

2
@ Frank.Germain在这种情况下,您可以基于两列使用唯一的外键RefIDRefType其中RefType是目标表的固定标识符。如果需要完整性,可以在触发器或应用程序层中进行检查。在这种情况下,可以进行常规检索。SQL应该允许这样的FK定义,使我们的生活更轻松。
djmj

2

另一个选择是,在Ticket第一列中指定拥有实体类型(UserGroup),第二列具有被引用UserGroupid,并且不使用外键,而是依靠触发器来强制引用完整性。

与Nathan的出色模型(上文)相比,我在这里看到两个优点:

  • 更直接的清晰度和简单性。
  • 编写更简单的查询。

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,我会把它们放在一起。


-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');

我认为这是代表您想要的东西的最通用方法,而不是使用标志。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.