【发布时间】:2019-08-04 17:51:57
【问题描述】:
在 DynamoDB 中对这些关系建模的最佳方法是什么?
- 一对一的关系
- 一对多关系
- 多对多关系
【问题讨论】:
-
自从我写这个链接以来,它就已经包含在答案中了。
标签: amazon-dynamodb
在 DynamoDB 中对这些关系建模的最佳方法是什么?
【问题讨论】:
标签: amazon-dynamodb
我已经多次看到这个问题的变体,我想我会写一个问答。
在阅读本文之前,您应该了解:
我们可以对护照和人员进行建模来展示这种关系。一本护照只能拥有一个人,一个人只能拥有一本护照。
方法很简单。我们有两张表,其中一张表应该有一个外键。
护照表:
分区键:PassportId
╔════════════╦═══════╦════════════╗
║ PassportId ║ Pages ║ Issued ║
╠════════════╬═══════╬════════════╣
║ P1 ║ 15 ║ 11/03/2009 ║
║ P2 ║ 18 ║ 09/02/2018 ║
╚════════════╩═══════╩════════════╝
护照持有人表:
分区键:PersonId
╔══════════╦════════════╦══════╗
║ PersonId ║ PassportId ║ Name ║
╠══════════╬════════════╬══════╣
║ 123 ║ P1 ║ Jane ║
║ 234 ║ P2 ║ Paul ║
╚══════════╩════════════╩══════╝
请注意,PersonId 没有出现在护照表中。如果这样做,我们将有 两个 具有相同信息的地方(哪些护照属于哪个人)。如果表格没有就谁拥有哪本护照达成一致,这将导致额外的数据更新和潜在的一些数据质量问题。
但是,我们缺少一个用例。我们可以通过 PersonId 轻松查找一个人,并找到他们拥有的护照。但是如果我们有一个 PassportId 并且我们需要找到它的所有者呢?在当前模型中,我们需要在 Passport holder 表上执行Scan。如果这是一个常规用例,我们就不想使用 Scan。要支持GetItem,我们可以简单地将GSI 添加到 Passport holder 表中:
护照持有人表 GSI:
分区键:PassportId
╔════════════╦══════════╦══════╗
║ PassportId ║ PersonId ║ Name ║
╠════════════╬══════════╬══════╣
║ P1 ║ 123 ║ Jane ║
║ P2 ║ 234 ║ Paul ║
╚════════════╩══════════╩══════╝
现在我们可以使用 PassportId 或 PersonId 快速且廉价地查找关系。
对此建模还有其他选项。例如,您可以有一个没有外键的“普通” Passport 表和 Person 表,然后有第三个辅助表将 PassortIds 和 PersonIds 简单地映射在一起。在这种情况下,我认为这不是最干净的设计,但是如果您喜欢它,那么这种方法没有任何问题。请注意,它们是多对多关系部分中辅助关系表的示例。
我们可以模拟宠物和主人来展示这种关系。宠物只能拥有一个主人,但主人可以拥有多只宠物。
该模型看起来与一对一模型非常相似,因此我将只关注这些差异。
宠物桌:
分区键:PetId
╔═══════╦═════════╦════════╗
║ PetId ║ OwnerId ║ Type ║
╠═══════╬═════════╬════════╣
║ P1 ║ O1 ║ Dog ║
║ P2 ║ O1 ║ Cat ║
║ P3 ║ O2 ║ Rabbit ║
╚═══════╩═════════╩════════╝
所有者表:
分区键:OwnerId
╔═════════╦════════╗
║ OwnerId ║ Name ║
╠═════════╬════════╣
║ O1 ║ Angela ║
║ O2 ║ David ║
╚═════════╩════════╝
我们将外键放在 many 表中。如果我们反过来做,并将 PetIds 放在 Owner 表中,一个 Owner Item 必须有一组 PetIds,这样管理起来会很复杂。
如果我们想找出宠物的主人,这很容易。我们可以通过GetItem 来返回宠物物品,它会告诉我们主人是谁。但反过来更难——如果我们有 OwnerId,他们拥有哪些 Pets?为了节省我们必须在 Pet 表上执行 Scan,我们改为将 GSI 添加到 Pet 表中。
宠物桌 GSI
分区键:OwnerId
╔═════════╦═══════╦════════╗
║ OwnerId ║ PetId ║ Type ║
╠═════════╬═══════╬════════╣
║ O1 ║ P1 ║ Dog ║
║ O1 ║ P2 ║ Cat ║
║ O2 ║ P3 ║ Rabbit ║
╚═════════╩═══════╩════════╝
如果我们有 OwnerId 并且想要找到他们的 Pets,我们可以在 Pet 表 GSI 上执行Query。例如,对所有者 O1 的查询将返回 PetId 为 P1 和 P2 的项目。
您可能会在这里注意到一些有趣的事情。主键对于表必须是唯一的。这仅适用于 基表。 GSI 主键,在本例中为 GSI partition key, does not have to be unique。
在 DynamoDB 表中,每个键值都必须是唯一的。然而,关键 全局二级索引中的值不需要是唯一的
附带说明,GSI 不需要project 与基表相同的所有属性。如果您仅将 GSI 用于查找,您可能希望仅投影 GSI 关键属性。
在 DynamoDB 中为多对多关系建模的主要方法有三种。各有优缺点。
我们可以使用医生和患者的例子来模拟这种关系。一个医生可以有很多病人,一个病人可以有很多医生。
一般来说,这是我首选的方法,这就是为什么它先行。这个想法是创建没有关系引用的“普通”基表。然后关系引用进入辅助表(每种关系类型一个辅助表 - 在这种情况下只是医生-患者)。
医生桌:
分区键:DoctorId
╔══════════╦═══════╗
║ DoctorId ║ Name ║
╠══════════╬═══════╣
║ D1 ║ Anita ║
║ D2 ║ Mary ║
║ D3 ║ Paul ║
╚══════════╩═══════╝
病床
分区键:PatientId
╔═══════════╦═════════╦════════════╗
║ PatientId ║ Name ║ Illness ║
╠═══════════╬═════════╬════════════╣
║ P1 ║ Barry ║ Headache ║
║ P2 ║ Cathryn ║ Itchy eyes ║
║ P3 ║ Zoe ║ Munchausen ║
╚═══════════╩═════════╩════════════╝
DoctorPatient 表(辅助表)
分区键:DoctorId
排序键:PatientId
╔══════════╦═══════════╦══════════════╗
║ DoctorId ║ PatientId ║ Last Meeting ║
╠══════════╬═══════════╬══════════════╣
║ D1 ║ P1 ║ 01/01/2018 ║
║ D1 ║ P2 ║ 02/01/2018 ║
║ D2 ║ P2 ║ 03/01/2018 ║
║ D2 ║ P3 ║ 04/01/2018 ║
║ D3 ║ P3 ║ 05/01/2018 ║
╚══════════╩═══════════╩══════════════╝
DoctorPatient 表 GSI
分区键:PatientId
排序键:DoctorId
╔═══════════╦══════════╦══════════════╗
║ PatientId ║ DoctorId ║ Last Meeting ║
╠═══════════╬══════════╬══════════════╣
║ P1 ║ D1 ║ 01/01/2018 ║
║ P2 ║ D1 ║ 02/01/2018 ║
║ P2 ║ D2 ║ 03/01/2018 ║
║ P3 ║ D2 ║ 04/01/2018 ║
║ P3 ║ D3 ║ 05/01/2018 ║
╚═══════════╩══════════╩══════════════╝
共有三个表,DoctorPatient 辅助表是有趣的。
DoctorPatient 基表主键必须是唯一的,因此我们创建了 DoctorId(分区键)和 PatientId(排序键)的复合键。
我们可以使用 DoctorId 在 DoctorPatient 基表上执行Query 以获取 Doctor 拥有的所有患者。
我们可以使用 PatientId 在 DoctorPatient GSI 上执行Query,以获取与患者关联的所有医生。
这种方法的优点是表的清晰分离,以及将简单业务对象直接映射到数据库的能力。它不需要使用更高级的功能,例如集合。
有必要协调一些更新,例如如果您删除了一个 Patient,您还需要小心删除 DoctorPatient 表中的关系。然而,与其他一些方法相比,引入数据质量问题的可能性较低。
编辑:DynamoDB 现在支持Transactions,允许您将多个更新协调到跨多个表的单个原子事务中。
这种方法的一个潜在弱点是它需要 3 个表。如果您正在为具有吞吐量的表提供服务,那么表越多,您就必须越薄地分散您的容量。然而,有了新的按需功能,这不是问题。
这种方法只使用两个表。
医生桌:
分区键:DoctorId
╔══════════╦════════════╦═══════╗
║ DoctorId ║ PatientIds ║ Name ║
╠══════════╬════════════╬═══════╣
║ D1 ║ P1,P2 ║ Anita ║
║ D2 ║ P2,P3 ║ Mary ║
║ D3 ║ P3 ║ Paul ║
╚══════════╩════════════╩═══════╝
病床:
分区键:PatientId
╔═══════════╦══════════╦═════════╗
║ PatientId ║ DoctorIds║ Name ║
╠═══════════╬══════════╬═════════╣
║ P1 ║ D1 ║ Barry ║
║ P2 ║ D1,D2 ║ Cathryn ║
║ P3 ║ D2,D3 ║ Zoe ║
╚═══════════╩══════════╩═════════╝
这种方法涉及将关系作为一个集合存储在每个表中。
要查找医生的患者,我们可以使用 Doctor 表上的 GetItem 来检索 Doctor 项目。然后将 PatientIds 作为一组存储在 Doctor 属性中。
要查找患者的医生,我们可以使用患者表上的 GetItem 来检索患者项目。然后将 DoctorId 作为一组存储在 Patient 属性中。
这种方法的优势在于业务对象和数据库表之间存在直接映射。只有两个表,所以如果您使用的是预置吞吐能力,则不需要分布得太细。
这种方法的主要缺点是可能存在数据质量问题。如果将患者链接到医生,则需要协调两个更新,每个表一个。如果一次更新失败会怎样?您的数据可能会不同步。
另一个缺点是在两个表中都使用了 Set。 DynamoDB SDK 旨在处理 Set,但涉及 Set 时,某些操作可能会很复杂。
AWS 之前将其称为Adjacency List pattern。它通常被称为Graph database 或Triple Store。
我之前在 AWS Adjancey List Pattern 中使用过 answered this question,这似乎有助于一些人理解它。
AWS 最近的一次演讲中对这种模式进行了很多讨论here
该方法涉及将所有数据放在一张表中。
我只是画了一些示例行而不是整个表格:
分区键:Key1
排序键:Key2
╔═════════╦═════════╦═══════╦═════════════╦══════════════╗
║ Key1 ║ Key2 ║ Name ║ illness ║ Last Meeting ║
╠═════════╬═════════╬═══════╬═════════════╬══════════════╣
║ P1 ║ P1 ║ Barry ║ Headache ║ ║
║ D1 ║ D1 ║ Anita ║ ║ ║
║ D1 ║ P1 ║ ║ ║ 01/01/2018 ║
╚═════════╩═════════╩═══════╩═════════════╩══════════════╝
然后需要一个 GSI 来反转密钥:
分区键:Key2
排序键:Key1
╔═════════╦═════════╦═══════╦═════════════╦══════════════╗
║ Key2 ║ Key1 ║ Name ║ illness ║ Last Meeting ║
╠═════════╬═════════╬═══════╬═════════════╬══════════════╣
║ P1 ║ P1 ║ Barry ║ Headache ║ ║
║ D1 ║ D1 ║ Anita ║ ║ ║
║ P1 ║ D1 ║ ║ ║ 01/01/2018 ║
╚═════════╩═════════╩═══════╩═════════════╩══════════════╝
此模型在某些特定情况下具有一些优势 - 它可以在高度连接的数据中表现良好。如果你很好地格式化你的数据,你可以实现非常快速和可扩展的模型。它很灵活,您可以在表中存储任何实体或关系,而无需更新架构/表。如果您要配置吞吐量容量,它可能会很高效,因为所有吞吐量都可用于跨应用程序的任何操作。
如果使用不当或没有认真考虑,此模型会出现一些巨大的缺点。
您失去了业务对象和表之间的任何直接映射。这几乎总是导致无法阅读的意大利面条代码。即使执行简单的查询也会感觉非常复杂。由于代码和数据库之间没有明显的映射关系,因此管理数据质量变得很困难。我见过的大多数使用这种方法的项目最终都会编写各种实用程序,其中一些本身就是产品,只是为了管理数据库。
另一个小问题是模型中每个项目的每个属性都必须存在于一个表中。这通常会生成一个包含数百列的表。这本身不是问题,但尝试处理具有这么多列的表通常会引发简单的问题,例如难以查看数据。
简而言之,我认为 AWS 可能已经在一组文章中发布了本应有用的文章,但由于未能介绍其他(更简单的)用于管理多对多关系的概念,它们让很多人感到困惑。需要明确的是,邻接列表模式可能很有用,但它不是在 DynamoDB 中建模多对多关系的唯一选择。如果它适用于您的情况(例如严重的大数据),请务必使用它,但如果不适用,请尝试其中一种更简单的模型。
【讨论】:
You should maintain as few tables as possible in a DynamoDB application. Most well designed applications require only one table. 并且他们映射多对多关系的最佳实践集中在一个表解决方案上,即邻接列表模式。对此有何看法?