【问题标题】:How to enforce DB integrity with non-unique foreign keys?如何使用非唯一外键强制数据库完整性?
【发布时间】:2023-03-18 18:19:01
【问题描述】:

我想要一个数据库表来保存带有修订历史的数据(如维基百科上的页面)。我认为一个好主意是有两列来标识行:(name, version)。所以示例表如下所示:

TABLE PERSONS:
    id:      int,
    name:    varchar(30),
    version: int,
    ... // some data assigned to that person.

因此,如果用户想要更新人员的数据,他们不会进行更新 - 相反,他们会创建一个具有相同 name 但不同 version 值的新 PERSONS 行。向用户显示的数据(对于给定的name)是具有最高version 的数据。

我有第二个表,比如 DOGS,它引用了 PERSONS 表中的人员:

TABLE DOGS:
    id:         int,
    name:       varchar(30),
    owner_name: varchar(30),
    ...

显然,owner_name 是对 PERSONS.name 的引用,但我不能将其声明为外键(在 MS SQL Server 中),因为 PERSONS.name 不是唯一的!

问题:那么,在 MS SQL Server 2008 中,我应该如何确保数据库完整性(即,对于每只 DOG,PERSONS 中至少存在一行,这样它的 PERSON.name = = DOG.owner_name)?

我正在寻找最优雅的解决方案——我知道我可以在 PERSONS 表上使用触发器,但这并不像我想要的那样具有声明性和优雅性。有什么想法吗?


其他信息

上面的设计有以下优点,如果我需要,我可以“记住”一个人当前的id(或(name, version)对),并且我确信该行中的数据永远不会改变。这很重要,例如如果我将这个人的数据作为文档的一部分然后打印出来,并且在 5 年内有人可能想要打印一份完全不变的副本(例如,使用与今天相同的数据),那么这对他们来说将很容易做到.

也许您可以想出一个完全不同的设计来实现相同的目的,并且可以更轻松地执行其完整性(最好使用外键或其他约束)?


编辑:感谢迈克尔·加图索的回答,我发现了另一种描述这种关系的方式。有两种解决方案,我将其发布为答案。请投票选出你更喜欢哪一个。

【问题讨论】:

    标签: sql sql-server foreign-keys unique


    【解决方案1】:

    在您的父表中,对 (id, version) 创建一个唯一约束。将版本列添加到您的子表,并使用检查约束确保它始终为 0。使用 FK 约束将 (parentid, version) 映射到您的父表。

    【讨论】:

    • 太棒了!子表中的数据有点多,但绝对值得——为设计带来简单和有序。谢谢!
    【解决方案2】:

    或者,您可以为具有历史价值的数据维护人员历史表。通过这种方式,您可以保持 Persons 和 Dogs 表的整洁和简单的引用,而且还可以访问历史上有趣的信息。

    【讨论】:

    • 是的,我可以这样做,但是我应该将当前数据保存在两份副本中:一份在 CurrentPerson 中,一份(最高版本)在 PersonH​​istory 中,这样我就不必切换了当从要打印的文档中指向一个人时,在表格之间。这意味着我必须确保这两个条目包含相同的数据……就是这样!你的解决方案给了我一个好主意!谢谢!我将其作为单独的答案发布。
    【解决方案3】:

    好的,首先您需要规范化您的表格。谷歌“数据库规范化”,你会想出大量的阅读。 PERSONS 表尤其需要注意。

    第二件事是,当您创建外键引用时,99.999% 的时间您想要引用一个 ID(数字)值。即,[DOGS].[owner] 应该是对 [PERSONS].[id] 的引用。

    编辑:添加示例模式(请原谅松散的语法)。我假设每只狗只有一个主人。这是实现个人历史的一种方式。所有列都不为空。

    Persons Table:
    int Id
    varchar(30) name
    ...
    
    PersonHistory Table:
    int Id
    int PersonId (foreign key to Persons.Id)
    int Version (auto-increment)
    varchar(30) name
    ...
    
    Dogs Table:
    int Id
    int OwnerId (foreign key to Persons.Id)
    varchar(30) name
    ...
    

    最新版本的数据将直接存储在 Persons 表中,旧数据存储在 PersonH​​istory 表中。

    【讨论】:

    • 哎呀,好痛!仅供参考,我非常了解标准化的含义。如果您知道如何解决我的问题,请更详细地解释它,并可能提供至少部分解决方案。
    • @DzinX: "首先,关于标准化,好吧,我可以扔掉 "PERSONS.id", ..." -- 这不是标准化。
    • 它是,因为 (name, version) 对意味着 id,反之亦然,所以我可以丢弃 id 或 version(因为 name 被 DOGS 表引用)。出于我帖子中解释的原因,我想保留该版本,所以我唯一可以丢弃的就是 id。但这对我一点帮助都没有!我看不到您想到的解决方案。你会对我的表应用什么规范化,它有什么帮助?
    • @DzinX:除非您在原始帖子中省略了某些内容,否则没有理由相信(名称、版本)对是唯一的。
    • 引用:“我认为最好有两列来标识行:(名称,版本)。”除此之外,据我所知,您的解决方案类似于 Mark 的解决方案,并且遇到同样的问题:不能保证每只 Dog 至少存在一个 PersonVersion(通过 Persons 表链接)。这可以通过某种方式解决吗?
    【解决方案4】:

    我会使用关联表将多个版本链接到一个 pk。

    【讨论】:

    • 您能详细解释一下吗?您的解决方案与 Mark 的类似吗?
    【解决方案5】:

    我参与的一个项目解决了类似的问题。这是一个生物记录数据库,随着新研究提高对分类学的理解,物种名称会随着时间而改变。

    但是,旧记录需要保持与原始物种名称的相关性。它变得复杂,但基本的解决方案是有一个只包含所有唯一物种名称的 NAME 表,一个代表实际物种的物种表和一个将两者联系在一起的 NAME_VERSION 表。在任何时候都会有一个首选名称(即当前接受的物种科学名称),它是 name_version 中保存的布尔字段。

    在您的示例中,这将转换为 Details 表(detailsid、otherdetails 列)、名为 DetailsVersion(detailsid、personid)的链接表和 Person 表(personid、不变数据)。将狗与人联系起来。

    【讨论】:

    • 嗯,从我所见,我现在有四个表,但仍然不能保证存在对应于 Dog 行的 Details 行。我错过了什么吗?
    【解决方案6】:

    人员

    id (int),
    姓名,
    .....
    activeVersion(这将是来自 personVersionInfo 的 UID)

    注意:上表将有每个人的 1 行。将具有创建人的原始信息。

    PersonVersionInfo

    UID(识别人员+版本的唯一标识符),
    id (int),
    姓名,
    .....
    versionId(这将为每个人生成)

    狗ID,
    狗名
    ......

    PersonsWithDogs

    UID,
    狗号

    编辑:您必须加入 PersonWithDogs、PersionVersionInfo、Dogs 才能获得全貌(截至今天)。这种结构将帮助您将狗链接到所有者(具有特定版本)。

    如果人的信息发生变化并且您希望获得与狗关联的最新信息,则必须更新 PersonWithDogs 表以获得给定狗所需的(人的)UID。

    您可以有一些限制,例如 DogID 在 PersonWithDogs 中应该是唯一的。
    而在这种结构中,一个UID(人)可以有很多狗。

    您的场景(可以更改/限制等)将有助于更好地设计架构。

    【讨论】:

    • 一只狗也可以有多个主人,这种情况也会发生。
    • @HLGEM:是的,这就是原因,我说“您的方案(可以更改/限制等)将有助于更好地设计架构”:)
    • 我不喜欢每次更改 Person 信息时都必须更新 PersonsWithDogs 的方式。这样,我可以有我的 2 个起始表并使用 DOGS.owner_id 而不是 DOGS.owner_name 并在每个人的更改时更新此 id。如果一个人有很多狗,这可能会很昂贵。
    【解决方案7】:

    感谢迈克尔·加图索的回答,我发现了另一种描述这种关系的方式。有两种解决方案,这是其中的第一种。请投票选出你更喜欢哪一个。

    解决方案 1

    在 PERSONS 表中,我们只留下姓名(唯一标识符)和指向当前人数据的链接:

    TABLE PERSONS:
        name:            varchar(30),
        current_data_id: int
    

    我们创建一个新表 PERSONS_DATA,其中包含该人的所有数据历史记录:

    TABLE PERSONS_DATA:
        id:      int
        version: int (auto-generated)
        ... // some data, like address, etc.
    

    DOGS 表保持不变,它仍然指向一个人的名字(FK 到 PERSONS 表)。

    优点:对于每只狗,至少存在一个 PERSONS_DATA 行,其中包含其所有者的数据(这就是我想要的)

    缺点:如果你想改变一个人的数据,你必须:

    1. 添加新的 PERSONS_DATA 行
    2. 更新此人的 PERSONS 条目以指向新的 PERSONS_DATA 行。

    【讨论】:

      【解决方案8】:

      感谢迈克尔·加图索的回答,我发现了另一种描述这种关系的方式。有两种解决方案,这是其中的第二种。请投票选出你更喜欢哪一个。

      解决方案 2

      在 PERSONS 表中,我们只留下姓名(唯一标识符)和指向第一个(不是当前!)人员数据的链接:

      TABLE PERSONS:
          name:            varchar(30),
          first_data_id: int
      

      我们创建一个新表 PERSONS_DATA,其中包含该人的所有数据历史记录:

      TABLE PERSONS_DATA:
          id:      int
          name:    varchar(30)
          version: int (auto-generated)
          ... // some data, like address, etc.
      

      DOGS 表保持不变,它仍然指向一个人的名字(FK 到 PERSONS 表)。

      优势:

      • 对于每只狗,至少存在一个 PERSONS_DATA 行,其中包含其所有者的数据(这正是我想要的)
      • 如果我想更改一个人的数据,我不必更新 PERSONS 行,只需添加一个新的 PERSONS_DATA 行

      缺点:要检索当前人员的数据,我必须:

      • 选择具有给定名称和最高版本的 PERSONS_DATA(可能很昂贵)
      • 选择具有特殊版本的 PERSONS_DATA,例如“-1”,但是每次添加新的 PERSONS_DATA 时我都必须更新两个 PERSONS_DATA 行,并且在这个解决方案中,我想避免必须更新 2 行...

      你怎么看?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多