防止数据库设计中的循环引用






4.76/5 (13投票s)
循环引用可能会导致您的数据不一致,
引言
当我们开始设计数据库时,我们都会考虑范式以及为了优化性能而进行的去范式化技术。为了保护信息和加强数据库的完整性,我们应用约束和业务规则。问题是,仅仅让数据库安全是否就足够了?
遵循范式和严格的约束还不够。假设您已经完美地完成了设计工作,一切看起来都很合理。但有一天,您会在一个正在运行的数据库中发现一个设计缺陷。而且,这个缺陷并不是因为错误地遵守了范式。它仅仅是逻辑设计问题。“循环引用”就是这样一个缺陷。
在本文中,我将通过一些实际工作中的例子来演示“循环引用”对数据一致性的影响。
示例 1
这个例子完全是关于任务管理的。我们首先来看构成一个循环的四个表:“Project
”、“Team
”、“Task
”和“ProjectAssignment
”。理论上,看起来没问题,因为它符合范式。在逻辑上,它有问题,因为这个设计隐藏了其潜在的不一致性缺陷。

在这个图中,出现了循环引用。数据可能面临风险,因为如果我们深入研究实体“ProjectAssignment
”和“Project
”之间的业务关系,数据将通过两条路径从 ProjectAssignment
流向 Project
。
ProjectAssignment
=>ProjectTeam
=>Project
ProjectAssignment
=>Task
=>Project
显然,通过任一路径查询的结果可能不同,导致数据流的不一致。
你可以问自己一个问题:“这个问题到底是怎么回事?我的系统仍然运行正常,没有受到这个问题的影响。我一点也不在乎。”
也许你是对的。你可能没有看到幕后的威胁。但这肯定会让你担忧,或者为你的业务带来不确定性。总有一天,数据不一致会发生。如果系统投入生产后不得不更改数据库设计,那将是一个真正的噩梦。让我们假设添加了以下需求:
“列出所有拥有角色为‘Supervisor’的团队成员的项目”
考虑到这个需求,如果我们不够小心地编写代码来强制执行严格的数据验证,最终可能会导致“ProjectTeam
”和“Task
”实体之间的数据不一致,其后果是查询结果可能不同。
解决方案: 有一些解决方案取决于业务需求。在初步设计阶段,最好留意构成四个以上表的循环引用。
在上面的例子中,我们可以提出 2 个解决方案:
解决方案 1: 如果“ProjectTeam
”表不必要,则删除该表。只有当需求暗示每个团队成员至少必须属于一个特定的任务时,此解决方案才有效。
解决方案 2: 如果需求暗示某些成员可以提前加入项目,直到被分配任务之前不需要属于任何特定任务,那么您需要权衡所有方面的利弊。如果您确信您的代码是稳健的,并且数据库中绝对没有任何异常,那么您可能不需要进行更改。相反,如果您的代码不够稳健,并且很可能发生意外更改,例如并发错误、传输错误、硬盘崩溃或遗漏设计场景,那么数据就不足够可靠。让我们看看这个场景:“将某个成员从一个项目移动到另一个项目”。显然,如果该团队成员已经被分配了某些任务,那么这是有风险的,除非已经施加了严格的规则/约束。此时您可以轻松地找出不一致数据是如何产生的。当然,您提出的规则/约束越严格,数据库就越安全。然而,一开始就找到所有严格的规则/约束从来都不是一件容易的事。此外,过多的约束和规则会带来处理开销,并阻碍创造性工作。
选择最合适的解决方案取决于您。因为您比别人更了解需求,只有您知道什么最适合您的数据库。在这种情况下,我只想证明定义如此多的约束不一定是最好的解决方案,同时我们仍然可以摆脱循环引用。因此,为了安全起见,数据库设计需要进行一些更改。
第二个解决方案有点棘手。我们仍然可以删除“ProjectTeam
”。但是,我们必须想出替代规则。以下规则可能有用:当创建一个新项目时,我们总是在“Task
”表中添加一个“特殊任务”,并通过 ProjectID
链接到 Project。有很多方法可以将记录标记为特殊记录。在这种情况下,将“TaskType
”的值设置为“Null
”是一种解决方案。这个特殊任务充当“虚拟任务”。它属于每个团队成员。现在,我们可以在“ProjectAssignment
”中为某个成员添加一个新记录,该记录通过(虚拟任务)链接“User
”和“Task
”表。显然,该成员不需要属于任何特定任务,但肯定属于一个项目。
在这个修改后的设计中,从一个实体到另一个实体的数据流不再超过一条路径。此外,我们现在删除了不必要的关联表“ProjectTeam
”,从而消除了冗余数据。这个解决方案也有助于解释“特殊记录”的重要性。缺点是使用“特殊记录”确实需要付出代价。开发者很容易忽略它,尤其是在维护阶段。这是一个权衡。
示例 2
第一个例子中发现的缺陷不是很危险,数据库设计中的更改可能不需要。但是这个例子中发现的缺陷告诉我们,需要进行更改。让我们看看需求:
- 每个客户可以同时购买多个产品,也可以在不同时间购买相同的产品。所有这些购买交易都存储在“
Purchase
”表中。 - 经销商可以从每位客户的每件已售产品中赚取佣金。所有佣金数据都存储在“
Commission
”表中。
正如第一个例子中所提到的,当用户尝试直接向数据库插入数据时,可能会发生数据不一致。在这个例子中,即使在 UI 页面中输入数据也可能导致问题。第一个例子更多的是不一致性而不是真正的威胁。而这个例子中的循环引用可能具有潜在危险性。
看看下面的设计。现在不一致的数据是由最终用户自己创建的。问题在于两个突出显示的行共享相同的 product (Bamboo Flooring)。此外,所有这些交易都属于同一个客户 (Pham Dinh Truong)。
现在,“Commission Amount
”处理起来存在真正的瓶颈。同一个客户在不同日期购买同一产品,每次购买的数量不同,因此佣金金额也可能不同。一旦为上述每个突出显示的行输入了 Commission Amount(自动或手动),在保存并重新加载网格后,Commission Amount 对这两个突出显示的行来说总是相同的。查询看起来像:
SELECT CommissionAmount FROM Commission
WHERE CustomerID = @CustomerID AND ProductID = @ProductID
此查询返回两个值,分别对应两个购买交易(两行),但是哪个 CommissionAmount
值对应哪个交易?您可以轻松猜到,返回的第一个值将用于两个交易。现在您知道发生了什么。
显然,这个 bug 很难检测。其后果是可能会扭曲我们的业务数据。
原始原因
让我们试着找出这个循环引用背后的原因。与第一个例子不同,这个例子总是确保实体之间的数据流只有一条路径。让我们回顾一下上面的 UI 部分,很明显 Purchase 和 Commission 之间存在隐藏的“业务关系”。然而,这种关系并没有反映在数据库设计中。
最后,我们找到了根本原因。最大的错误是遗漏了隐藏的关系。因此,数据库开发人员无意中创建了一个导致数据不一致的循环引用。
解决方案
那么这个问题该如何解决呢?
更改数据库设计会造成失控的损害,尤其是在系统已经上线运行时。如果产品上线后更改设计,我们必须付出代价。迟早,循环引用会导致麻烦,给最终用户使用系统带来不确定性。因此,我们需要有敏锐的洞察力来检测问题并尽快解决它。
看看以下修改后的数据库设计,Customer
-Commission
和 Product
-Commission
表对之间的关系已被打破。Commission 表现在链接到 Purchase
表。
显然,修改后的设计比原始设计更好。
结论
为了消除您对数据库设计(尤其是数据不一致性)的疑虑,以下建议可能有所帮助:
- 循环引用会增加错误和不一致的可能性。尽量减少循环引用的数量,以最大程度地降低对数据一致性和正确性的影响。
- 重要的是在实施阶段尽早检测和防止循环引用,以免情况变得糟糕。
- 确保从一个实体到另一个实体的数据流只有一条路径。
- 在某些情况下,使用“特殊记录”可能有助于数据库设计。
检测设计缺陷从来都不容易。我们不确定运行中的数据库中是否存在异常。这需要敏锐的洞察力才能在数据库设计中找到罪魁祸首。希望本文能帮助您保持对数据库设计的敏锐观察。熟能生巧。
历史
- 版本 1.0 (2009 年 8 月 1 日) - 初始发布