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

时间:2019-03-13 22:48:25

标签: amazon-dynamodb

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

  • 一对一关系
  • 一对多关系
  • 多对多关系

1 个答案:

答案 0 :(得分:6)

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

DynamoDB关键基础

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

  • 每个DynamoDB表都有唯一的primary key
  • 主键必须由partition key组成,并且可以有一个sort key。既有分区键又有排序键的主键是复合键
  • GetItem请求使用其唯一的主键只返回一项。
  • Query进行快速查找,并且必须指定一个且只有一个分区键。它可以返回多个项目。
  • Scan评估表中的每个项目,并可能基于过滤器参数返回一个子集。在某些情况下,扫描是正确的选择,但如果使用不正确,则扫描速度会很慢且成本很高。
  • GSILSI之间的区别以及它们的用途。

一对一


我们可以为护照和人物建模以证明这种关系。一本护照只能有一个所有者,一个人只能拥有一本护照。

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

护照表:

分区键: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持有人表上执行Scan。如果这是常规用例,则我们不希望使用扫描。为了支持GetItem,我们可以简单地将GSI添加到Passport持有人表:

护照持有人表格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  ║
╚═════════╩════════╝

我们将外键放在许多表中。如果相反,将PetIds放在Owner表中,则一个Owner Item将必须具有一组PetId,这将使管理变得复杂。

如果我们想找出宠物的主人,这很容易。我们可以执行GetItem来返回宠物物品,它告诉我们主人是谁。但是另一种方法则更难-如果我们拥有OwnerId,他们拥有哪些宠物?为了节省我们必须在Pet表上执行Scan,我们改为在Pet表中添加一个GSI。

宠物桌GSI

分区键:OwnerId

╔═════════╦═══════╦════════╗
║ OwnerId ║ PetId ║ Type   ║
╠═════════╬═══════╬════════╣
║ O1      ║ P1    ║ Dog    ║
║ O1      ║ P2    ║ Cat    ║
║ O2      ║ P3    ║ Rabbit ║
╚═════════╩═══════╩════════╝

如果我们有一个OwnerId,并且想要找到它们的宠物,则可以在宠物表GSI上执行Query。例如,对所有者O1的查询将返回带有PetId P1和P2的项目。

您可能会在这里注意到一些有趣的事情。一个主键对于一个表必须是唯一的。这仅对基本表正确。 GSI主键,在本例中为GSI partition key, does not have to be unique

  

在DynamoDB表中,每个键值必须唯一。但是,关键   全局二级索引中的值不必唯一

另一方面,GSI不需要project与基表相同的所有属性。如果仅将GSI用于查找,则可能只希望投影GSI键属性。


多对多


在DynamoDB中,有三种主要的方式来建模多对多关系。每个都有优点和缺点。

我们可以使用“医生和患者”的示例来建立这种关系的模型。医生可以有很多病人,而病人可以有很多医生。


多对多-选项1-辅助表


通常,这是我的首选方法,这就是为什么它首先出现的原因。这个想法是创建没有关系引用的“普通”基表。然后将关系引用放入辅助表(每种关系类型一个辅助表-在这种情况下,仅是Doctors-Patients)。

医生表:

分区密钥: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   ║
╚═══════════╩══════════╩══════════════╝

有3个表,DoctorPatient辅助表是有趣的表。

DoctorPatient基表主键必须唯一,因此我们创建了DoctorId(分区键)和PatientId(排序键)的复合键。

我们可以使用DoctorId在DoctorPatient基表上执行Query,以获取Doctor拥有的所有患者。

我们可以使用PatientId在DoctorPatient GSI上执行Query,以获取与患者关联的所有Doctors。

这种方法的优势在于表的清晰分隔以及将简单业务对象直接映射到数据库的能力。它不需要使用更高级的功能(例如集合)。

有必要协调一些更新,例如,如果删除“患者”,则还需要注意删除DoctorPatient表中的关系。但是,与其他方法相比,引入数据质量问题的机会较低。

此方法的潜在缺点是需要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项目。然后,将PatientId作为一组存储在Doctor属性中。

要查找患者的医生,我们可以在“患者”表上使用GetItem来检索“患者”项目。然后,DoctorIds作为一组存储在Patient属性中。

这种方法的优势在于,业务对象和数据库表之间存在直接映射。只有两个表,因此,如果您使用预配置吞吐容量,则不必将其分布得太窄。

此方法的主要缺点是潜在的数据质量问题。如果将“患者”链接到“医生”,则需要协调两个更新,每个表一个。如果一次更新失败会怎样?您的数据可能会不同步。

另一个缺点是在两个表中都使用了Set。 DynamoDB SDK设计用于处理Set,但是当涉及Set时,某些操作可能会很复杂。


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


AWS先前将其称为Adjacency List pattern。通常将其称为Graph databaseTriple Store

我以前在answered this question上有过AWS Adjancey列表模式,它似乎帮助了一些人理解它。

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