前一阵在Stack Overflow 上看到一篇关于DynamoDB 建模的回答,回答很好。所以这里整理一篇文章回顾该回答,指导以后的设计。
1 Dynamodb 基础概念
- 每个DynamoDB表都有一个唯一的主键。
- 主键必须由分区键组成,并且可以选择性地具有排序键。同时具有分区键和排序键的主键是复合键。
- 一次GetItem请求返回一个且唯一一个使用它的唯一主键项。
- 一个查询做了快速查找,必须指定一个且只有一个分区键。它可以返回多个项目(排序键不同)。
- 一个扫描评估表中的每个项目,并可能返回基于过滤器参数的一个子集。在某些情况下,扫描是正确的选择,但如果使用不当可能会很慢并且代价高昂。
- GSI(全局二级索引)和LSI(本地二级索引)之间的区别以及它们的用途。
2 一对一
我们可以为护照和持有人建模以证明这种关系。一本护照只能有一个主人,一个人只能拥有一本护照。
我们有两个表,其中一个表应该有一个外键。
护照表:
分区键: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找到对应PassportId ,但这里耶存在问题,如果知道PaasportId,是没有办法找到对应的PersonId的,一对一要实现正反向均有效的情况的话需要做一定的处理。所以护照持有人表上建立一个以PassportId为主键全局二级索引GSI即可。
护照持有人表GSI:
分区键:PassportId
╔════════════╦══════════╦══════╗
║ PassportId ║ PersonId ║ Name ║
╠════════════╬══════════╬══════╣
║ P1 ║ 123 ║ Jane ║
║ P2 ║ 234 ║ Paul ║
╚════════════╩══════════╩══════╝
现在,就可以非常快速且廉价地使用PassportId或PersonId查找关系。
3 一对多
我们可以为宠物和主人建模以证明这种关系。宠物只能拥有一个主人,但主人可以拥有许多宠物。
宠物表:
分区键:PetId
╔═══════╦═════════╦════════╗
║ PetId ║ OwnerId ║ Type ║
╠═══════╬═════════╬════════╣
║ P1 ║ O1 ║ Dog ║
║ P2 ║ O1 ║ Cat ║
║ P3 ║ O2 ║ Rabbit ║
╚═══════╩═════════╩════════╝
宠物拥有者表:
分区键:OwnerId
╔═════════╦════════╗
║ OwnerId ║ Name ║
╠═════════╬════════╣
║ O1 ║ Angela ║
║ O2 ║ David ║
╚═════════╩════════╝
我们把外键放在很多表中。如果我们反过来这样做,并将PetIds放在Owner表中,则一个所有者项必须有一组PetId,这将变得很复杂。
如果我们想找一个宠物的主人,它很容易。我们可以做一个GetItem来返回Pet Item,它会告诉我们拥有者是谁。但另一种方式是更难 - 如果我们有一个OwnerId,他们拥有哪些宠物?为了节省我们不必在Pet表上进行扫描,我们将GSI添加到Pet表中。
宠物表拥有者ID GSI
分区键:OwnerId
╔═════════╦═══════╦════════╗
║ OwnerId ║ PetId ║ Type ║
╠═════════╬═══════╬════════╣
║ O1 ║ P1 ║ Dog ║
║ O1 ║ P2 ║ Cat ║
║ O2 ║ P3 ║ Rabbit ║
╚═════════╩═══════╩════════╝
如果我们有一个OwnerId并且我们想找到他们的宠物,我们可以在宠物拥有者ID GSI上执行查询。例如,对所有者O1的查询将返回具有PetId P1和P2的项目。
你可能会注意到这里有趣的事情主键对于表必须是唯一的。这仅适用于基表。GSI主键(在本例中仅为GSI分区键)不必是唯一的。
另外,GSI不需要投影与基表相同的所有属性。如果您仅使用GSI进行查找,则可能仅投影希望GSI键属性。(PS:省空间)
3 多对多
在DynamoDB中有三种主要方式来建模多对多关系。每个人都有优点和缺点。
我们可以使用医生和患者的例子来建模这种关系。医生可以有很多患者,患者可以有很多医生。
PS:AWS 官方其实有所推荐,但那种推荐方式真的是比较恶心。
3.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基表执行查询,以获取Doctor所具有的所有患者。
我们可以使用PatientId对DoctorPatient GSI 执行查询,以获取与患者相关的所有医生。
这种方法的优点是表的清晰分离,以及将简单业务对象直接映射到数据库的能力。它不需要使用诸如集之类的更高级功能。
有必要协调一些更新,例如,如果删除患者,您还需要小心删除DoctorPatient表中的关系。然而,与其他一些方法相比,引入数据质量问题的可能性很低。也就是删除实体需要删除关系表。DynamoDB 支持事务操作可以处理这个问题。
这种方法的潜在弱点是它需要3个表。如果您使用吞吐量配置表,则表中的表越多,您就越需要扩展容量。然而,随着新的按需功能,这不是一个问题。
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的患者,我们可以在Doctor表上使用GetItem来检索Doctor项。然后,PatientIds将作为一组存储在Doctor属性中。
要查找患者的医生,我们可以在患者表上使用GetItem来检索患者项目。然后将DoctorIds作为一组存储在Patient属性中。
这种方法的优势在于业务对象和数据库表之间存在直接映射。只有两个表,因此如果您使用提供吞吐量容量,则不需要将容量分散太开。
这种方法的主要缺点是数据质量问题的可能性。如果您将患者链接到医生,您需要协调两个更新,每个表更新一个。如果一次更新失败会怎样?您的数据可能会不同步。(PS: 解决两表同步这里就需要引入DynamoDB事务**, DynamoDB的事务本身就意味着双倍的容量开销)。
另一个缺点是在两个表中使用集合(操作集合数据结构中的某些内容)。DynamoDB SDK旨在处理集合,但在涉及集合时,某些操作可能会很复杂。
3.3 图形模式
AWS先前已将此称为邻接列表模式。它通常被称为Graph数据库或Triple Store。这是AWS推荐的官方方案,基于NoSql的情况下,**该方法涉及将所有数据放在一个表中。**因为NoSql并不对每条数据类型都要求一致。
这是绘制了一些示例行而不是整个表:
分区键:Key1
排序键:键2
╔═════════╦═════════╦═══════╦═════════════╦══════════════╗
║ Key1 ║ Key2 ║ Name ║ illness ║ Last Meeting ║
╠═════════╬═════════╬═══════╬═════════════╬══════════════╣
║ P1 ║ P1 ║ Barry ║ Headache ║ ║
║ D1 ║ D1 ║ Anita ║ ║ ║
║ D1 ║ P1 ║ ║ ║ 01/01/2018 ║
╚═════════╩═════════╩═══════╩═════════════╩══════════════╝
然后需要一个GSI来反转键:
分区键:Key2
排序键:键1
╔═════════╦═════════╦═══════╦═════════════╦══════════════╗
║ Key2 ║ Key1 ║ Name ║ illness ║ Last Meeting ║
╠═════════╬═════════╬═══════╬═════════════╬══════════════╣
║ P1 ║ P1 ║ Barry ║ Headache ║ ║
║ D1 ║ D1 ║ Anita ║ ║ ║
║ P1 ║ D1 ║ ║ ║ 01/01/2018 ║
╚═════════╩═════════╩═══════╩═════════════╩══════════════╝
该模型在某些特定情况下具有一些优势 - 它可以在高度连接的数据中表现良好。如果您很好地格式化数据,则可以实现极其快速且可扩展的模型。它非常灵活,您可以在表中存储任何实体或关系,而无需更新架构/表。如果您正在配置吞吐量容量,则可以高效**,因为整个应用程序中的所有操作都可以使用所有吞吐量。**
如果使用不当或没有认真考虑,这个模型会有一些巨大的缺点。
您丢失了业务对象和表之间的任何直接映射。这几乎总是导致不可读的代码。执行甚至简单的查询都会感觉非常复杂。由于代码和数据库之间没有明显的映射,因此管理数据质量变得困难。设置为了管理数据库会编写相应的工具,因为完全没有数据库模型和代码无法建立直接的映射。
所以要明确,邻接列表模式可能很有用,但它不是在DynamoDB中建模多对多关系的唯一选择。一定要使用它,如果它适用于你的情况,如严重的大数据,但如果没有,尝试一个更简单的模型。PS:事实当时读DynamoDB官方多对多关系文档时感觉一阵懵逼,如果没有足够优秀健壮且具有前瞻性的数据库设计,而就采用这种邻接表形式存储数据,可能会导致数据库变得十分难以维护。而AWS官方本身并没有对该方案的这种情况进行详细说明,虽然邻接表性能会有所优势且节约资源。
个人偏向于前两种多对多方案。