如何在 DynamoDB 中建模一对一、一对多和多对多关系

Posted

技术标签:

【中文标题】如何在 DynamoDB 中建模一对一、一对多和多对多关系【英文标题】:How to model one-to-one, one-to-many and many-to-many relationships in DynamoDB 【发布时间】:2019-08-04 17:51:57 【问题描述】:

在 DynamoDB 中对这些关系建模的最佳方法是什么?

一对一的关系 一对多关系 多对多关系

【问题讨论】:

AWS 推荐 m:n docs.aws.amazon.com/amazondynamodb/latest/developerguide/… 自从我写这个链接以来,它就已经包含在答案中了。 【参考方案1】:

我已经多次看到这个问题的变体,我想我会写一个问答。

DynamoDB 关键基础知识

在阅读本文之前,您应该了解:

每个 DynamoDB 表都有一个唯一的primary key 主键必须由partition key 组成,并且可以有一个可选的sort key。同时具有分区键和排序键的主键是复合键GetItem 请求使用其唯一的主键返回一项且仅一项。 Query 进行快速查找,并且必须指定一个且仅一个分区键。它可以返回多个项目。 Scan 评估表中的每个项目,并可能根据过滤器参数返回一个子集。在某些情况下,扫描是正确的选择,但如果使用不当,可能会很慢且代价高昂。 全局二级索引 (GSI) 具有与基表不同的分区键。可以把它想象成有两个保持同步的表(基表和 GSI)。根据使用情况,GSI 可能会使您的基表成本翻倍。 本地二级索引 (LSI) 具有与基表相同的分区键,但排序键不同。将其视为对基表数据进行排序的另一种方式,但仅限于分区键内。 LSI 不会花费您任何费用。

一对一


我们可以对护照和人员进行建模来展示这种关系。一本护照只能拥有一个人,一个人只能拥有一本护照。

方法很简单。我们有两张表,其中一张表应该有一个外键。

护照表:

分区键: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 中为多对多关系建模的主要方法有三种。各有优缺点。

我们可以使用医生和患者的例子来模拟这种关系。一个医生可以有很多病人,一个病人可以有很多医生。


多对多 - 选项 1 - 辅助表


一般来说,这是我首选的方法,这就是为什么它先行。这个想法是创建没有关系引用的“普通”基表。然后关系引用进入辅助表(每种关系类型一个辅助表 - 在这种情况下只是医生-患者)。

医生桌:

分区键: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 个表。如果您正在为具有吞吐量的表提供服务,那么表越多,您就必须越薄地分散您的容量。然而,有了新的按需功能,这不是问题。


多对多 - 选项 2 - 外键集


这种方法只使用两个表。

医生桌:

分区键: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 时,某些操作可能会很复杂。


多对多 - 选项 3 - 图表架构


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 中建模多对多关系的唯一选择。如果它适用于您的情况(例如严重的大数据),请务必使用它,但如果不适用,请尝试其中一种更简单的模型。

【讨论】:

我们倾向于将选项 1 用于多对多,有时也使用选项 2,其中许多的一侧是一个简单的属性,如符号。对于一对多,还有一个同时具有分区键和排序键的单表解决方案,其中排序键捕获关系多方的标识符。虽然这越来越接近于一个表来统治他们所有的解决方案,但我认为如果您总是将父数据和子数据一起加载并且您永远不需要为孩子找到父母,它会带来好处。它基本上是 select * 解决方案。 AWS 文档声明 You should maintain as few tables as possible in a DynamoDB application. Most well designed applications require only one table. 并且他们映射多对多关系的最佳实践集中在一个表解决方案上,即邻接列表模式。对此有何看法? 此外,这里介绍的所有一对多和多对多解决方案都需要写入多个表和/或表中的项目,而 DynamoDB 不能保证操作是原子的。使用 NoSQL 时,数据冗余是必须的,这会自动导致数据可能出现不一致。也许您的回答应该更加强调这一点? 当 AWS 文档明确指出最佳实践时,这个答案似乎出人意料地偏向于 反对 邻接列表模式,并且 反对 使用单个表是尽可能少的桌子。对我来说,一对一和一对多的关系绝对应该使用分区键和排序键来完成,并且可以选择使用 GSI 来在另一个方向查找数据。 我发现这个答案对学习数据库设计原则很有帮助,但正如其他人确实评论的那样,这不是 DynamoDB 推荐的工作流程。我想我会分享两个我发现有用的资源(对于相对初学者来说),以帮助我了解最佳实践;本帖:alexdebrie.com/posts/dynamodb-single-table 本教程:aws.amazon.com/getting-started/hands-on/…

以上是关于如何在 DynamoDB 中建模一对一、一对多和多对多关系的主要内容,如果未能解决你的问题,请参考以下文章

如何判断一对一对多和多对多的关系

Hibernate映射关系:一对一对多和多对多

Sonata admin 一对多和多对一显示列表

简述数据库设计中一对多和多对多的应用场景?

Spring Boot项目中的MongoDB一对多和多对一关系

一对多和多对一的关系,用mybatis写