第四章 – 数据建模

最后更新于:2022-04-01 00:55:00

让我们换换思维,对 MongoDB 进行一个更抽象的理解。介绍一些新的术语和一些新的语法是非常容易的。而要接受一个以新的范式来建模,是相当不简单的。事实是,当用新技术进行建模的时候,我们中的许多人还在找什么可用的什么不可用。在这里我们只是开始新的开端,而最终你需要去在实战中练习和学习。 与大多数 NoSQL 数据库相比,面向文档型数据库和关系型数据库很相似 - 至少,在建模上是这样的。但是,不同点非常重要。 ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#no-joins)No Joins 你需要适应的第一个,也是最根本的区别就是 mongoDB 没有链接(join) 。我不知道 MongoDB 中不支持链接的具体原因,但是我知道链接基本上意味着不可扩展。就是说,一旦你把数据水平扩展,无论如何你都要放弃在客户端(应用服务器)使用链接。事实就是,数据 _有_ 关系, 但 MongoDB 不支持链接。 没别的办法,为了在无连接的世界生存下去,我们只能在我们的应用代码中自己实现链接。我们需要进行二次查询 `find`,把相关数据保存到另一个集合中。我们设置数据和在关系型数据中声明一个外键没什么区别。先不管我们那美丽的`unicorns` 了,让我们来看看我们的 `employees`。 首先我们来创建一个雇主 (我提供了一个明确的 `_id` ,这样我们就可以和例子作成一样) ~~~ db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d730"), name: 'Leto'}) ~~~ 然后让我们加几个工人,把他们的管理者设置为 `Leto`: ~~~ db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d731"), name: 'Duncan', manager: ObjectId( "4d85c7039ab0fd70a117d730")}); db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d732"), name: 'Moneo', manager: ObjectId( "4d85c7039ab0fd70a117d730")}); ~~~ (有必要再重复一次, `_id` 可以是任何形式的唯一值。因为你很可能在实际中使用 `ObjectId` ,我们也在这里用它。) 当然,要找出 Leto 的所有工人,只需要执行: ~~~ db.employees.find({manager: ObjectId( "4d85c7039ab0fd70a117d730")}) ~~~ 这没什么神奇的。在最坏的情况下,大多数的时间,为弥补无链接所做的仅仅是增加一个额外的查询(可能是被索引的)。 ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#数组和内嵌文档)数组和内嵌文档 MongoDB 不支持链接不意味着它没优势。还记得我们说过 MongoDB 支持数组作为文档中的基本对象吗?这在处理多对一(many-to-one)或者多对多(many-to-many)的关系的时候非常方便。举个简单的例子,如果一个工人有两个管理者,我们只需要像这样存一下数组: ~~~ db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d733"), name: 'Siona', manager: [ObjectId( "4d85c7039ab0fd70a117d730"), ObjectId( "4d85c7039ab0fd70a117d732")] }) ~~~ 有趣的是,对于某些文档,`manager` 可以是单个不同的值,而另外一些可以是数组。而我们原来的 `find` 查询依旧可用: ~~~ db.employees.find({manager: ObjectId( "4d85c7039ab0fd70a117d730")}) ~~~ 你会很快就发现,数组中的值比多对多链接表(many-to-many join-tables)要容易处理得多。 数组之外,MongoDB 还支持内嵌文档。来试试看向文档插入一个内嵌文档,像这样: ~~~ db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId( "4d85c7039ab0fd70a117d730")}}) ~~~ 像你猜的那样,内嵌文档可以用 dot-notation 查询: ~~~ db.employees.find({ 'family.mother': 'Chani'}) ~~~ 我们只简单的介绍一下内嵌文档适用情况,以及你怎么使用它们。 结合两个概念,我们甚至可以内嵌文档数组: ~~~ db.employees.insert({_id: ObjectId( "4d85c7039ab0fd70a117d735"), name: 'Chani', family: [ {relation:'mother',name: 'Chani'}, {relation:'father',name: 'Paul'}, {relation:'brother', name: 'Duncan'}]}) ~~~ ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#反规范化denormalization)反规范化(Denormalization) 另外一个代替链接的方案是对你的数据做反规范化处理(denormalization)。从历史角度看,反规范化处理是为了解决那些对性能敏感的问题,或是需要做快照的数据(比如说审计日志)。但是,随着日益增长的普及的 NoSQL,对链接的支持的日益丧失,反规范化作为规范化建模的一部分变得越来越普遍了。这不意味着,应该对你文档里的每条数据都做冗余处理。而是说,与其对冗余数据心存恐惧,让它影响你的设计决策,不如在建模的时候考虑什么信息应当属于什么文档。 比如说,假设你要写一个论坛应用。传统的方式是通过 `posts` 中的 `userid` 列,来关联一个特定的 `user` 和一篇 `post`。这样的建模,你没法在显示 `posts` 的时候不查询 (链接到) `users`。一个代替案是简单的在每篇 `post` 中把 `name` 和`userid` 一起保存。你可能要用到内嵌文档,比如 `user: {id: ObjectId('Something'), name: 'Leto'}`。是的,如果你让用户可以更新他们的名字,那么你得对所有的文档都进行更新(一个多重更新)。 适应这种方法不是对任何人都那么简单的。很多情况下这样做甚至是无意义的。不过不要害怕去尝试。它只是在某些情况下不适用而已,但在某些情况下是最好的解决方法。 ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#你的选择是)你的选择是? 在处理一对多(one-to-many)或者多对多(many-to-many)场景的时候,id 数组通常是一个正确的选择。但通常,新人开发者在面对内嵌文档和 "手工" 引用时,左右为难。 首先,你应该知道的是,一个独立文档的大小当前被限制在 16MB 。知道了文档的大小限制,挺宽裕的,对你考虑怎么用它多少有些影响。在这点上,看起来大多数开发者都愿意手工维护数据引用关系。内嵌文档经常被用到,大多数情况下多是很小的数据块,那些总是被和父节点一起拉取的数据块。现实的例子是为每个用户保存一个 `addresses` ,看起来像这样: ~~~ db.users.insert({name: 'leto', email: 'leto@dune.gov', addresses: [{street: "229 W. 43rd St", city: "New York", state:"NY",zip:"10036"}, {street: "555 University", city: "Palo Alto", state:"CA",zip:"94107"}]}) ~~~ 这并不意味着你要低估内嵌文档的能力,或者仅仅把他们当成小技巧。把你的数据模型直接映射到你的对象,这会使得问题更简单,并且通常也不需要用到链接了。尤其是,当你考虑到 MongoDB 允许你对内嵌文档和数组的字段进行查询和索引时,效果特别明显。 ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#大而全还是小而专的集合)大而全还是小而专的集合? 由于对集合没做任何的强制要求,完全可以在系统中用一个混合了各种文档的集合,但这绝对是个非常烂的主意。大多数 MongoDB 系统都采用了和关系型数据库类似的结构,分成几个集合。换而言之,如果在关系型数据库中是一个表,那么在 MongoDB 中会被作成一个集合 (many-to-many join tables being an important exception as well as tables that exist only to enable one to many relationships with simple entities)。 当你把内嵌文档考虑进来的时候,这个话题会变的更有趣。常见的例子就是博客。你是应该分成一个 `posts` 集合和一个`comments` 集合呢,还是应该每个 `post` 下面嵌入一个 `comments` 数组? 先不考虑那个 16MB 文档大小限制 ( _哈姆雷特_全文也没超过 200KB,所以你的博客是有多人气?),许多开发者都喜欢把东西划分开来。这样更简洁更明确,给你更好的性能。MongoDB 的灵活架构允许你把这两种方式结合起来,你可以把评论放在独立的集合中,同时在博客帖子下嵌入一小部分评论 (比如说最新评论) ,以便和帖子一同显示。这遵守以下的规则,就是你到想在一次查询中获取到什么内容。 这没有硬性规定(好吧,除了16MB限制)。尝试用不同的方法解决问题,你会知道什么能用什么不能用。 ## [](https://github.com/geminiyellow/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown#小结-3)小结 本章目标是提供一些对你在 MongoDB 中数据建模有帮助的指导, 一个新起点,如果愿意你可以这样认为。在一个面向文档系统中建模,和在面向关系世界中建模,是不一样的,但也没多少不同。你能得到更多的灵活性并且只有一个约束,而对于新系统,一切都很完美。你唯一会做错的就是你不去尝试。
';