【问题标题】:Foreign Key to multiple tables多个表的外键
【发布时间】:2011-12-12 06:25:54
【问题描述】:

我的数据库中有 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。如果您使用 Guids 作为键,整个事情也可以很好地工作

标签: sql-server relational-database


【解决方案1】:

您有几个选项,它们的“正确性”和易用性各不相同。与往常一样,正确的设计取决于您的需求。

  • 您可以简单地在 Ticket 中创建两列,OwnedByUserId 和 OwnedByGroupId,并为每个表设置可为空的外键。

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

  • 您可以为每个用户创建一个默认组,并让票证由真正的组或用户的默认组拥有。

  • 或者(我的选择)为作为用户和组的基础的实体建模,并拥有该实体拥有的票证。

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

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
)

【讨论】:

  • 用户/组票的查询是什么样的?谢谢。
  • 组和用户表中的持久计算列有什么好处? Party表中的主键已经保证Group Id和User Id不会重叠,所以外键只需要在PartyId上即可。无论如何编写的任何查询仍然需要知道来自 PartyTypeName 的表。
  • @ArinTaylor 持久化列阻止我们创建用户类型的 Party 并将其与 dbo.Group 中的记录相关联。
  • @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,有人可以确认这是反模式还是反模式的解决方案?
【解决方案2】:

@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 上的主键对于这个特定的实现来说不是必需的,但是在这样的表中拥有一个绝对不会有害。)

【讨论】:

  • 这也是我们软件中的内容,如果您尝试创建通用数据访问框架,我会避免这样做。这种设计会增加应用层的复杂性。
  • 我对 SQL 真的很陌生,如果有错误请纠正我,但是当您非常有信心只需要两种所有者类型的票证时,这种设计似乎是一种使用方法。今后,如果引入了第三种票证所有者类型,则必须向表中添加第三个可为空的外键列。
  • @Shadoninja:你没有错。事实上,我认为这是一种完全公平的说法。我通常对这种合理的解决方案感到满意,但在考虑选项时,我肯定不会首先想到它——正是因为你概述的原因。
  • @Frank.Germain 在这种情况下,您可以使用基于两列RefIDRefType 的唯一外键,其中RefType 是目标表的固定标识符。如果您需要完整性,您可以在触发器或应用程序层中进行检查。在这种情况下可以进行通用检索。 SQL 应该允许这样的 FK 定义,让我们的生活更轻松。
【解决方案3】:

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

与 Nathan 的 excellent model(上图)相比,我在这里看到了两个优势:

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

【讨论】:

  • 但这不允许外键对吧?我仍在尝试为我当前的项目找出正确的设计,其中一张表可以引用至少 3 个,将来可能更多
  • 鉴于触发器需要锁定表才能可靠地执行检查,这不会损害多用户性能吗?
【解决方案4】:

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

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 出错了,我只是把它放在一起。

【讨论】:

    【解决方案5】:

    您还可以使用枚举来识别Owner 是用户还是组,如下所示:

    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 TYPE Enum_OwnerType AS ENUM ('Group', 'User');
    CREATE TABLE dbo.Ticket
    (
        ID int NOT NULL,
        Owner int NOT NULL,
        OwnerType Enum_OwnerType NOT NULL,
        Subject varchar(50) NULL
    )
    

    也许它并不比任何提议的解决方案更好,它可能不会提供任何优势。事实上,我认为这可能需要更改Enum_OwnerType 甚至ticket 才能更改OwnerType,我猜……我希望它还是有用的。

    【讨论】:

      【解决方案6】:

      我有很多这样的情况,我只是使用如下的多态能力:

      示例

      我有包含idamountuser_id 列的营业额表,我需要知道每条记录的引用,所以我只需添加两个字段 table_idtable_type 以及我的最终营业额表就像idamountuser_idtable_idtable_type

      • 如果新记录是关于这样插入的订单记录 [1,25000,2,22,order]
      • 如果新记录是关于像这样的增量信用 [1,25000,2,23,credit]

      注意

      如果使用 M:M 表,两次检索记录需要花费大量时间 和我的方式

      • 缺点是营业额表记录数增长
      • Pons 在新记录和可读性和搜索能力方面更加灵活

      【讨论】:

        【解决方案7】:
        CREATE TABLE dbo.OwnerType
        (
            ID int NOT NULL,
            Name varchar(50) NULL
        )
        
        insert into OwnerType (Name) values ('User');
        insert into OwnerType (Name) values ('Group');
        

        我认为这将是表示您想要的内容而不是使用标志的最通用方式。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-11-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-05-19
          • 2012-06-18
          相关资源
          最近更新 更多