SQL Server 指向多个表的外键

yftpprvb  于 2023-01-20  发布在  其他
关注(0)|答案(8)|浏览(107)

我的数据库里有三个相关的表。

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字段,票证可以归组或用户所有。
描述票证与可选用户或组之间关系的“最正确”方式是什么?
我想我应该在ticket表中添加一个标志,说明什么类型拥有它。

dxxyhpgq

dxxyhpgq1#

你有几个选择,都在“正确性”和易用性上有所不同。一如既往,正确的设计取决于你的需要。

  • 您可以简单地在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
)
lnvxswe2

lnvxswe22#

@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,这就是为什么两个都必须是可空的)。
(The Ticket.ID上的主键对于这个特定的实现不是必需的,但是在这样的表中有一个主键肯定不会有什么坏处。)

ryhaxcpt

ryhaxcpt3#

另一种方法是创建一个关联表,其中包含每个潜在资源类型的列。在您的示例中,两个现有所有者类型中的每一个都有自己的表(这意味着您有一些要参考的内容)。如果始终是这种情况,则您可以创建如下内容:

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将没有外键约束,但将用作对其他表的引用。缺点是,您必须查看该表以了解所有者类型,因为根据架构,所有者类型并不立即显而易见。只有在您事先不知道所有者类型并且它们不会“Don“不要链接到其他表。如果您事先知道所有者类型,我会使用类似于@Nathan Skerl的解决方案。
抱歉,如果我有一些SQL错误,我只是扔在一起。

s6fujrry

s6fujrry4#

还有一种选择是在Ticket中有一列指定拥有实体类型(UserGroup),第二列具有引用的UserGroup id,并且不使用外键,而是依赖触发器来实施引用完整性。
与Nathan的excellent model(上图)相比,我在这里看到了两个优点:

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

2w3rbyxf5#

你也可以使用一个枚举来标识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,我猜...我希望它是有用的无论如何。

piah890a

piah890a6#

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

示例

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

  • 如果新记录与如下插入的顺序记录有关[ 125000222order ]
  • 如果新记录是关于增量信用,如[ 125000223credit ]

注解

如果使用M:M表,它需要这么多的时间,两个检索记录和我的方式

  • 缺点是营业额表记录数越来越多
  • 脑桥在新记录和可读性以及搜索能力方面更灵活
nnt7mjpx

nnt7mjpx7#

nathan_jr的第四个选项(建模一个实体,作为用户和组的基础,并拥有该实体所拥有的票据)不强制PartyId的引用完整性。你必须在应用层这样做,这会引起各种各样的麻烦。当django的genericforeignkey实现相同的解决方案时,不能称之为反模式。但毫无疑问,你可以使用框架的orm(使用类似于django的多表继承)设计出更健壮、性能更高的东西

8oomwypt

8oomwypt8#

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

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

我认为这将是最通用的方式来表示你想要什么,而不是使用一个标志。

相关问题