如何在 MongoDB 中设计文档模式

作者选择了 开放式互联网 / 自由言论基金作为 写给捐赠计划的一部分接受捐款。

介绍

如果在关系数据库中工作的经验很多,那么可能很难超越关系模型的原则,比如在表格和关系上思考. [以文件为导向的数据库] (https://www.digitalocean.com/community/conceptual_articles/an-introduction-to-document-oriented-databases)类似蒙戈DB,可以摆脱关系模型的僵硬性和局限性. 然而,在数据库中储存自我描述文件所带来的灵活性和自由可能导致其他陷阱和困难.

本概念性文章概述了文档导向数据库中与方案设计相关的五个常见指导方针,并强调了在模拟数据之间的关系时应采取的各种考虑,还将探讨几种策略,可以用来模拟此类关系,包括嵌入分组中的文档和使用儿童和家长参考,以及这些策略最合适的使用时。

指南1 - 一起存储需要访问的内容

在典型的 关系数据库中,数据存储在表中,每个表构建一个固定的列表,代表构成实体、对象或事件的各种属性,例如,在代表大学的学生的表中,您可能会找到包含每个学生的姓名、姓名、出生日期和唯一识别号的列。

通常情况下,每个表代表一个单一的主题. 如果您想存储有关学生的当前研究,奖学金或以前的教育的信息,则可以合理地将这些数据保存在一个单独的表中,其中包含他们的个人信息。

例如,描述每个学生奖学金状态的表可以指学生的学生ID号码,但它不会直接存储学生的名字或地址,避免数据重复。

通过引用来描述关系的这种方法被称为 normalized data model. 以这种方式存储数据 - 使用多个单独、简洁的对象相互关联 - 也可能在文档导向的数据库中。

以文档为导向的数据库中建模数据的基本概念是存储将被访问到一起。`进一步挖掘到学生的例子中,说这个学校的大多数学生有超过一个电子邮件地址,这就是为什么大学希望能够存储多个电子邮件地址与每个学生的联系信息。

在这种情况下,一个示例文档可能具有如下结构:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ]
15}

注意,此示例文档包含嵌入式电子邮件地址列表。

代表一个单一文档中的多个主体的特征是一个 denormalized 数据模型. 它允许应用程序在一次中检索和操纵给定对象(在这里,一个学生)的所有相关数据,而无需访问多个单独的对象和集合。

使用嵌入式文档一起存储需要访问的内容通常是以文档为导向的数据库中表示数据的最佳方式. 在以下指南中,您将了解如何在文档为导向的数据库中最好地模拟对象之间的不同关系,例如对一个或对多个关系。

指南2 - 与嵌入式文档模拟一对一关系

_one-to-one 关系表示两个不同的对象之间的关联,其中一个对象与另一个类型的对象相连。

继续以上一节的学生为例,每个学生在任何特定的时间点都只有一个有效的学生身份证. 一张卡从来不属于多名学生,任何学生都不得拥有多张身份证. 如果将所有这些数据存储在一个关系数据库中,那么通过将学生记录和身份证记录存储在单独的表格中,通过引用将学生和身份证记录捆绑在一起,来模拟学生与身份证之间的关系,很可能是有意义的.

在文档数据库中表示此类关系的一种常见方法是使用嵌入式文档. 例如,以下文档描述了一个名叫Sammy的学生及其学生身份证:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "id_card": {
 6        "number": "123-1234-123",
 7        "issued_on": ISODate("2020-01-23"),
 8        "expires_on": ISODate("2020-01-23")
 9    }
10}

注意:本示例文档"id_card"字段所持的不是单一值,而是代表学生身份证的嵌入式文件,由身份证号码,卡的发卡日期,卡的失效日期来描述. 身份证基本上成为描述学生Sammy的文件的一部分,尽管这是现实生活中一个单独的物件. 通常,这样构建文档的图案,以便您通过单一查询获取所有相关信息,是一种声音选择.

如果您遇到连接某个类型的对象与其他类型的许多对象的关系,如学生的电子邮件地址,他们参加的课程或他们在学生委员会的信息板上发布的消息,情况就会变得不那么简单。

指南3 - 与嵌入式文档模拟一对几的关系

当一种类型的对象与另一种类型的多个对象相关时,它可以被描述为一对多的关系,学生可以有多个电子邮件地址,汽车可以有许多部件,或者购物订单可以由多个项目组成。

虽然在文档数据库中代表一对一关系的最常见方式是通过嵌入式文档,但在文档架构中模拟一对多关系的几种方法。

  • ** Cardinity ** :_Cardinity_是某一集中单个元素数的衡量. 例如,如果一个班级有30个学生,你可以说班级有30个基础. 在一个一对多的关系中,各式各样的重心可能有所不同. 学生可以有一个电子邮件地址或多个地址. 他们可能只注册几个班级,或者他们可以有一个完全的日程安排。 在一对多的关系中,"多"的大小会影响你如何模拟数据.
  • ** 独立访问** : 一些相关数据如果有的话,很少与主体分开访问. 例如,在没有其他学生细节的情况下检索一个学生的电子邮件地址可能并不常见. 另一方面,大学的课程可能需要单独访问和更新,无论注册参加这些课程的学生是谁。 您是否单独访问相关文档也会影响您如何建模数据 。 *** 数据之间的关系严格来说是一对多的关系** : 学生在大学学习的课程就是一个例子。 从学生的角度来看,他们可以参加多门课程. 从表面上看,这似乎是一对一的关系. 然而,大学课程很少由一名学生参加;更常见的是,有多个学生将参加同一班的学习. 在这样的情况下,所涉及的关系并不是真正的一对一关系,而是多对一关系,因此你会采取不同于一对一关系的模式. .

假设你正在决定如何存储学生电子邮件地址. 每个学生可以有多个电子邮件地址,例如一个用于工作,一个用于个人使用,一个由大学提供。

1{
2    "email": "[email protected]",
3    "type": "work"
4}

在基本性方面,每个学生只有几个电子邮件地址,因为学生不可能有几十个——更不要说几百个——电子邮件地址. 因此,这种关系可以被定性为"一对一"关系,这是将电子邮件地址直接嵌入学生文档并一起存储的令人信服的理由. 您不会冒任何风险 电子邮件地址列表会无限期地增长, 这将使得文档大 和低效的使用.

<$>[注] :请注意,存储数据在数组中存在某些陷阱,例如,单个MongoDB文档的大小不能超过16MB,虽然使用数组字段嵌入多个文档是可能的,但如果对象列表不受控制地增长,文档可以快速达到这个大小限制。

在一个数组字段中嵌入多个文档可能适用于许多情况,但要知道这可能并不总是最好的解决方案。

至于独立访问,电子邮件地址很可能不会被学生单独访问,因此,没有明确的激励,将它们作为单独的文档存储在一个单独的收藏中。

最后要考虑的是,这种关系是否真的是一对多人的关系,而不是一对多人的关系. 由于电子邮件地址属于一个人,所以将这种关系描述为一对多人的关系(或更准确地说,一对少数的关系)而不是一对多人的关系是合理的。

这三个假设表明,将学生的各种电子邮件地址嵌入到描述学生本身的相同文件中将是存储此类数据的良好选择。

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ]
15}

使用此结构,每次你检索学生的文档,你也会检索嵌入的电子邮件地址在同一读取操作。

如果您建模一对几种关系,而相关文档不需要独立访问,则像这样直接嵌入文档通常是可取的,因为这可以减少方案的复杂性。

但是,如前所述,插入此类文档并不总是最佳的解决方案,下一节提供了有关为什么在某些情况下可能发生这种情况的更多细节,并概述了如何使用儿童引用作为文档数据库中代表关系的替代方法。

指南4:用儿童参考来建模一对多和多对多关系

学生之间的关系的性质和他们的电子邮件地址告诉你,如何在文档数据库中最好地模拟这种关系. 这与学生和他们参加的课程之间的关系之间存在一些差异,所以你模拟学生和他们的课程之间的关系的方式也会有所不同。

描述一个学生参加的单一课程的文档可以遵循这样的结构:

1{
2    "name": "Physics 101",
3    "department": "Department of Physics",
4    "points": 7
5}

假设您从一开始就决定使用嵌入式文档来存储有关每个学生课程的信息,如本示例中所示:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ],
15    "courses": [
16        {
17            "name": "Physics 101",
18            "department": "Department of Physics",
19            "points": 7
20        },
21        {
22            "name": "Introduction to Cloud Computing",
23            "department": "Department of Computer Science",
24            "points": 4
25        }
26    ]
27}

这是一个完全有效的 MongoDB 文档,可以很好地服务于目的,但请考虑您在上一个指南中了解的三个关系属性。

第一种是枢机性. 学生可能只保留几个电子邮件地址,但他们可以在学习期间参加多个课程. 经过几年的参加,学生可能有几十个课程参加。

如果你决定像上一个例子那样嵌入每个课程,学生的文档很快就会变得模糊。

第二个考虑是独立访问. 与电子邮件地址不同,假设有时需要自行获取有关大学课程的信息是合适的。 例如,假设有人需要有关可用的课程的信息来准备营销小册子。 此外,课程可能需要随着时间的推移更新:教授教课程可能会发生变化,时间表可能会波动,或前提可能需要更新。

如果您将课程存储为学生文件中嵌入的文件,查找大学提供的所有课程的列表将变得麻烦。此外,每次课程需要更新时,您将需要浏览所有学生记录并在任何地方更新课程信息。

第三要考虑的是,学生与大学课程的关系到底是一对一,还是多对一. 在这种情况下,是后者,因为每个课程可以参加的学生超过一个. 这种关系的本质和独立访问方面表明,反对嵌入每个课程文件,主要出于方便访问和更新等实际原因。 考虑到课程和学生之间关系的多种性质,将课程文件储存在单独的文献库中,并存有他们自己独特的识别资料,也许是有道理的.

本单独集合中的代表类的文档可能具有类似于以下示例的结构:

 1{
 2    "_id": ObjectId("61741c9cbc9ec583c836170a"),
 3    "name": "Physics 101",
 4    "department": "Department of Physics",
 5    "points": 7
 6},
 7{
 8    "_id": ObjectId("61741c9cbc9ec583c836170b"),
 9    "name": "Introduction to Cloud Computing",
10    "department": "Department of Computer Science",
11    "points": 4
12}

如果你决定存储类似此类的课程信息,你需要找到一种方法来将学生连接到这些课程,这样你就可以知道哪些学生参加哪些课程。

与儿童参考,学生的文档将引用学生参加的课程的对象标识符在嵌入式数组中,如本示例中:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ],
15    "courses": [
16        ObjectId("61741c9cbc9ec583c836170a"),
17        ObjectId("61741c9cbc9ec583c836170b")
18    ]
19}

请注意,这个示例文件仍然有一个课程字段,也是一个阵列,但并没有像先前的例子那样将全部课程文件嵌入,而是只嵌入了单独收藏中引用课程文件的标识符。 现在,在检索学生文件时,课程不会立即提供,需要分别询问. 另一方面,人们立刻知道要收回哪些课程。 此外,如果任何课程的细节需要更新,则只需修改课程文件本身。 学生与其课程之间的所有参考都仍然有效.

<$>[注] 注: 没有明确的规则,当关系的枢纽性太大,以便以这种方式嵌入儿童引用。如果你是最适合该应用程序的,你可能会选择一种不同的方法以较低或更高的枢纽性选择。

如果您建模了一对多关系,其中相关文档的数量在合理的范围内,相关文档需要独立访问,建议将相关文档单独存储,并嵌入儿童引用来连接到它们。

现在你已经学会了如何使用儿童引用来表示不同类型的数据之间的关系,本指南将概述一个相反的概念:父母引用。

指南5 - 使用父母参考来建模无限的一对多关系

当有太多的相关对象将其直接嵌入到家长文档中时,使用儿童引用功能很好,但数量仍然处于已知的范围内。

举个例子,想象大学的学生理事会有一个信息板,任何学生都可以发布他们想要的信息,包括有关课程的问题,旅行故事,工作帖子,学习材料,或者只是免费聊天。

1{
2    "_id": ObjectId("61741c9cbc9ec583c836174c"),
3    "subject": "Books on kinematics and dynamics",
4    "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
5    "posted_on": ISODate("2021-07-23T16:03:21Z")
6}

您可以使用之前讨论的两种方法 - 嵌入和儿童参考 - 来模拟这种关系. 如果您决定嵌入,学生的文档可能会采取这样的形状:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ],
15    "courses": [
16        ObjectId("61741c9cbc9ec583c836170a"),
17        ObjectId("61741c9cbc9ec583c836170b")
18    ],
19    "message_board_messages": [
20        {
21            "subject": "Books on kinematics and dynamics",
22            "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
23            "posted_on": ISODate("2021-07-23T16:03:21Z")
24        },
25        . . .
26    ]
27}

然而,如果一个学生在写消息时富有成效,他们的文档很快就会变得非常长,并且可以轻松地超过16MB的大小限制,所以这种关系的核心性建议不包括嵌入。

<$>[注] ** 注意:** 您还应该考虑在获取学生的文档时是否经常访问信息板消息. 否则,如果所有信息都嵌入到该文档中,则在获取和操纵该文档时会受到性能处罚,即使邮件列表不经常使用。

现在考虑使用儿童引用,而不是像上一个示例一样嵌入完整的文档,每个消息将存储在一个单独的集合中,然后学生的文档可以有以下结构:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ],
15    "courses": [
16        ObjectId("61741c9cbc9ec583c836170a"),
17        ObjectId("61741c9cbc9ec583c836170b")
18    ],
19    "message_board_messages": [
20        ObjectId("61741c9cbc9ec583c836174c"),
21        . . .
22    ]
23}

在这个例子中,`message_board_messages'字段现在存储Sammy所写所有信件的儿童参考。 然而,改变方法只解决了前面提到的一个问题,因为现在可以独立访问信息。 但是,虽然使用儿童参考方法,学生的文档大小会增长得更慢,但鉴于这种关系的无约束性,收集对象标识符也可能变得无用. 毕竟,一个学生在学习的四年里可以轻松地写出上千条信息.

在这种情况下,通过 parent references 连接一个对象到另一个对象的一个常见方式,与之前描述的儿童引用不同,它现在不是学生文档,指向单个消息,而是消息文档中的引用,指向写它的学生。

若要使用父母引用,您需要修改消息文档方案,以包含引用创建该消息的学生:

1{
2    "_id": ObjectId("61741c9cbc9ec583c836174c"),
3    "subject": "Books on kinematics and dynamics",
4    "message": "Hello! Could you recommend a good introductory books covering the topics of kinematics and dynamics? Thanks!",
5    "posted_on": ISODate("2021-07-23T16:03:21Z"),
6    "posted_by": ObjectId("612d1e835ebee16872a109a4")
7}

请注意,新的posted_by字段包含学生文档的对象标识符,现在,学生的文档不会包含有关他们发布的消息的任何信息:

 1{
 2    "_id": ObjectId("612d1e835ebee16872a109a4"),
 3    "first_name": "Sammy",
 4    "last_name": "Shark",
 5    "emails": [
 6        {
 7            "email": "[email protected]",
 8            "type": "work"
 9        },
10        {
11            "email": "[email protected]",
12            "type": "home"
13        }
14    ],
15    "courses": [
16        ObjectId("61741c9cbc9ec583c836170a"),
17        ObjectId("61741c9cbc9ec583c836170b")
18    ]
19}

要获取由学生撰写的消息列表,你会使用消息收集的查询并过滤对posted_by字段. 将它们放在一个单独的集合中,可以安全地让消息列表增长而不会影响学生的任何文档。

<$>[注] ** 注意:** 使用父母引用时,在引用母文档的字段创建索引可以显著提高每次对母文档标识符进行过滤时的查询性能。

如果您建模了一对多关系,其中相关文档数量是无限的,无论文档是否需要独立访问,一般建议您单独存储相关文档,并使用家长参考来连接它们到主文档。

结论

由于面向文件的数据库具有灵活性,确定在文件数据库中建立关系模式的最佳方式不如在关系数据库中那样严格科学。 通过阅读这篇文章,你已经熟悉了嵌入文件,并使用孩子和父母的参考文献存储相关数据. 您已经学会了考虑关系至上性,避免无约束的数组,以及考虑文档是单独还是频繁访问.

这些只是几条指导方针,可以帮助您在 MongoDB 中建模典型的关系,但建模数据库方案不是一个尺寸适合所有。

要了解更多有关 MongoDB 中的不同类型的数据存储的方案设计和常见模式,我们建议您查看有关该主题的 官方 MongoDB 文档

Published At
Categories with 技术
comments powered by Disqus