离线下载
PDF版 ePub版

geminiyellow · 更新于 2017-12-11 08:00:54

数据建模

让我们换换思维,对 MongoDB 进行一个更抽象的理解。介绍一些新的术语和一些新的语法是非常容易的。而要接受一个以新的范式来建模,是相当不简单的。事实是,当用新技术进行建模的时候,我们中的许多人还在找什么可用的什么不可用。在这里我们只是开始新的开端,而最终你需要去在实战中练习和学习。

与大多数 NoSQL 数据库相比,面向文档型数据库和关系型数据库很相似 - 至少,在建模上是这样的。但是,不同点非常重要。

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")})

这没什么神奇的。在最坏的情况下,大多数的时间,为弥补无链接所做的仅仅是增加一个额外的查询(可能是被索引的)。

数组和内嵌文档

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'}]})

反规范化(Denormalization)

另外一个代替链接的方案是对你的数据做反规范化处理(denormalization)。从历史角度看,反规范化处理是为了解决那些对性能敏感的问题,或是需要做快照的数据(比如说审计日志)。但是,随着日益增长的普及的 NoSQL,对链接的支持的日益丧失,反规范化作为规范化建模的一部分变得越来越普遍了。这不意味着,应该对你文档里的每条数据都做冗余处理。而是说,与其对冗余数据心存恐惧,让它影响你的设计决策,不如在建模的时候考虑什么信息应当属于什么文档。

比如说,假设你要写一个论坛应用。传统的方式是通过 posts 中的 userid 列,来关联一个特定的 user 和一篇 post 。这样的建模,你没法在显示 posts 的时候不查询 (链接到) users。一个代替案是简单的在每篇 post 中把 nameuserid 一起保存。你可能要用到内嵌文档,比如 user: {id: ObjectId('Something'), name: 'Leto'}。是的,如果你让用户可以更新他们的名字,那么你得对所有的文档都进行更新(一个多重更新)。

适应这种方法不是对任何人都那么简单的。很多情况下这样做甚至是无意义的。不过不要害怕去尝试。它只是在某些情况下不适用而已,但在某些情况下是最好的解决方法。

你的选择是?

在处理一对多(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 允许你对内嵌文档和数组的字段进行查询和索引时,效果特别明显。

大而全还是小而专的集合?

由于对集合没做任何的强制要求,完全可以在系统中用一个混合了各种文档的集合,但这绝对是个非常烂的主意。大多数 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限制)。尝试用不同的方法解决问题,你会知道什么能用什么不能用。

小结

本章目标是提供一些对你在 MongoDB 中数据建模有帮助的指导, 一个新起点,如果愿意你可以这样认为。在一个面向文档系统中建模,和在面向关系世界中建模,是不一样的,但也没多少不同。你能得到更多的灵活性并且只有一个约束,而对于新系统,一切都很完美。你唯一会做错的就是你不去尝试。

上一篇: 掌握查询 下一篇: MongoDB 适用场景