多个表的外键

Dar*_*g8r 113 sql-server relational-database

我的数据库中有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
)
Run Code Online (Sandbox Code Playgroud)

用户属于多个组.这是通过多对多关系完成的,但在这种情况下无关紧要.票证可以由组或用户通过dbo.Ticket.Owner字段拥有.

什么是最正确的方式描述故障单和可选的用户或组之间的这种关系?

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

Nat*_*erl 139

您有几个选项,所有选项都有"正确性"和易用性.一如既往,正确的设计取决于您的需求.

  • 您可以在Ticket,OwnedByUserId和OwnedByGroupId中创建两个列,并且每个表都有可为空的Fks.

  • 您可以创建M:M参考表,同时启用ticket:user和ticket:组关系.也许将来您可能希望允许多个用户或组拥有单个票证?此设计不强制票证必须仅由单个实体拥有.

  • 您可以为每个用户创建一个默认组,并使票证仅由真实组或用户的默认组拥有.

  • 或者(我的选择)建模一个实体,它充当用户和组的基础,并拥有该实体拥有的票证.

下面是使用您发布的架构的粗略示例:

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
)
Run Code Online (Sandbox Code Playgroud)

  • 用户/组票的查询是什么样的?谢谢. (5认同)
  • @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;` 结果您将拥有每个票证主题和所有者姓名。 (4认同)
  • Group和User表中持久计算列的好处是什么?Party表中的主键已经确保Group Ids和User Ids中没有重叠,因此外键只需要单独在PartyId上.无论如何,编写的任何查询仍然需要知道PartyTypeName中的表. (3认同)
  • @ArinTaylor 持久列阻止我们创建 User 类型的 Party 并将其与 dbo.Group 中的记录相关联。 (2认同)
  • 我想在这里补充一件事。这实际上对通用应用程序框架的开发有很大帮助。由于您在组和用户中都有“名称”属性,因此您实际上应该在基本类型中移动该字段。否则非常好的例子! (2认同)
  • 非常完整的答案,这个例子很有趣而且非常普遍。不过,有一点要注意:您不需要:“unique (PartyId, PartyTypeId)”,因为“PartyId”已经是主键 (2认同)
  • 关于选项4,有人可以确认这是反模式还是反模式的解决方案? (2认同)

And*_*y M 28

@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
    )
);
Run Code Online (Sandbox Code Playgroud)

如您所见,该Ticket表有两列,OwnerGroup并且这两列OwnerUser都是可以为空的外键.(其他两个表中的相应列相应地成为主键.)CK_Ticket_GroupUser检查约束确保两个外键列中只有一个包含引用(另一个是NULL,这就是为什么两者都必须可为空).

(Ticket.ID对于这个特定的实现,主键不是必需的,但在这样的表中有一个肯定不会有害.)

  • 我对SQL真的很陌生,所以如果这是错误的,请更正我,但是当您非常有信心只需要两种所有者类型的票证时,这种设计似乎是一种使用方法。如果引入了第三种票证所有者类型,那么您将不得不在表中添加第三个可空外键列。 (3认同)
  • 这也是我们软件中的内容,如果您尝试创建通用数据访问框架,我会避免这样做。这种设计会增加应用层的复杂性。 (2认同)
  • @Frank.Germain 在这种情况下,您可以使用基于两列“RefID”和“RefType”的唯一外键,其中“RefType”是目标表的固定标识符。如果您需要完整性,您可以在触发器或应用程序层中进行检查。在这种情况下,通用检索是可能的。SQL 应该允许这样的 FK 定义,让我们的生活更轻松。 (2认同)

小智 5

另一种方法是创建一个关联表,其中包含每个潜在资源类型的列。在您的示例中,两个现有所有者类型中的每一个都有自己的表(这意味着您有一些可以引用的内容)。如果情况总是如此,你可以有这样的东西:

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
)

Run Code Online (Sandbox Code Playgroud)

使用此解决方案,您将在向数据库添加新实体时继续添加新列,并且删除并重新创建@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
)

Run Code Online (Sandbox Code Playgroud)

通过上述方法,您可以添加任意数量的所有者类型。Owner_ID 没有外键约束,但将用作对其他表的引用。缺点是您必须查看表才能了解所有者键入的内容,因为根据架构,它并不是立即显而易见的。仅当您事先不知道所有者类型并且它们不会链接到其他表时,我才会建议这样做。如果您事先知道所有者类型,我会采用像@Nathan Skerl 这样的解决方案。

抱歉,如果我弄错了一些 SQL,我只是把它们放在一起。