想要构建您自己的内容管理系统(CMS),也就是我们常说的博客吗?这是学习如何使用数据库时的经典案例,无论是关系型数据库管理系统(RDBMS)还是 NoSQL 数据库,因为它既涉及潜在的大量数据,也涉及数据之间的关系。博客应用的示例也非常适用于其他数据建模需求。
本文将探讨在 MongoDB 中设计 NoSQL 文档时的注意事项。不过我们不会真正开发一个博客应用,而是仅从数据的角度进行分析。
博客应用的组成部分
在尝试为博客设计文档模型之前,最好先退一步,思考每个可能与数据相关的组件。
一个典型的博客可能具有以下特性:
多个用户或作者
每位作者有多篇博文
每篇博文有多条评论
当然,根据您的业务需求,功能列表可能会更长、更复杂。为保持简单,我们将基于上述内容来建模。
可行但不推荐的做法
看到上述特性后,您的第一反应可能是将每个条目视为一个独立的文档。这是您在 Postgres 等关系型数据库中会采用的典型方法。在 MongoDB 中,它可能看起来像下面这样:
{
"_id" : "author-1" ,
"name" : "Nic Raboy" ,
"description" : "I'm some dude named Nic."
} 技术支持
以上是作者的一个大幅精简表示。当然,您还可以有许多其他字段,如 email 等等,但这在这里并不重要。
接着,我们可能会为博文设计如下结构:
{
"_id" : "blog-1" ,
"author_id" : "author-1" ,
"title" : "MongoDB is Awesome!" ,
"content" : "This is some blog content..."
} 技术支持
与前一个类似,博客文档还可以有其他字段,如 tags 等您能想到的内容。这里的重点是两个文档之间通过 id 进行关联。稍后我们会继续讨论。
该场景中的最后一个文档模型是评论:
{
"_id" : "comment-1" ,
"blog_id" : "blog-1" ,
"username" : "Anonymous User" ,
"content" : "Hey great article, it really helped a lot!"
} 技术支持
同样,在该场景中,评论文档与博客文档之间基于 id 值建立引用关系。这在关系型数据库中非常常见,实际上在 MongoDB 中也能正常工作。然而,这并不能带来最佳的 MongoDB 体验。
不建议在 MongoDB 中这样做的一个原因是性能问题。
要连接这三个文档,您需要在聚合管道中使用 $lookup 操作。$lookup 并不便宜,随着数据库规模扩大,它会对性能产生负面影响,尤其是当管道中有多个 $lookup 操作时。
在 MongoDB 中,有更好的解决方案。
更优、更符合 MongoDB 思维的数据建模方式
MongoDB 的一条基本规则是:一起被访问的数据应尽量存放在一起。将每个功能拆分到独立的文档或集合会违背这条规则,因为向用户展示的数据不再能通过一次获取完成。
让我们换个思路,为博客建模如下:
{
"_id" : "author-1" ,
"name" : "Nic Raboy" ,
"description" : "I'm some dude named Nic." ,
"posts" : [
{
"title" : "MongoDB is Awesome!" ,
"content" : "This is some blog content..." ,
"comments" : [
{
"username" : "Anonymous User" ,
"content" : "Hey great article, it really helped a lot!"
}
]
}
]
} 技术支持
好的,上述模型满足了 MongoDB 的一条主要规则。我们在一个文档中使用嵌套来把该作者的所有博文与评论放在一起。
虽然可行,但把博客应用的文档这样建模可能并不是好主意。
MongoDB 存在文档大小限制,而当文档中包含无界数组时,您有可能触及这些限制。更不用说数组变大后可能带来的性能问题。在此例中,我们可能有无限数量的博文以及每篇博文无限数量的评论。某个时刻会变得难以维护。
在这个示例中,更好的方法可能是将两种策略加以组合。
请记住,作者、博文和评论都可能有任意数量的字段。在本例中,我们保持了最小化。考虑到这一点,我们知道最常被访问的数据将是博文本身。来看一下新的设计:
{
"_id" : "blog-1" ,
"author" : {
"_id" : "author-1" ,
"name" : "Nic Raboy" ,
} ,
"title" : "MongoDB is Awesome!" ,
"content" : "This is some blog content..." ,
"comments" : [
{
"username" : "Anonymous User" ,
"content" : "Hey great article, it really helped a lot!"
}
]
} 技术支持
本例中的第二个文档仍然是作者信息的原始文档:
{
"_id" : "author-1" ,
"name" : "Nic Raboy" ,
"description" : "I'm some dude named Nic."
} 技术支持
我们知道在展示一篇博文时应包含部分作者信息,但不一定需要全部信息。我们可以在博文中嵌入所需的作者信息以及 _id,如果用户想了解更多作者信息,可以在您的应用中进一步查看。这样就不必每次都进行 $lookup 操作,从而提升性能。
现在,我们面临的问题是:某篇博文可能存在无限数量的评论,而这些评论目前是以数组形式附加的。
有几种处理方法,最终取决于您的选择:
可以采取最朴素的做法,将评论保留为数组,但这通常不是好主意。
可以回到最初的设计:每条评论一个文档,均带有指向博文的引用 id,并通过 $lookup 将其包含进来。
可以使用归档策略:在博文中以数组形式存储固定数量的评论,同时为每条评论创建单独的评论文档,只有在需要时才查询额外的评论。
可以探索分桶(bucketing)模式 ,将评论按每个文档约 100 条分组为“桶”,一次查询所需数量的评论桶。分桶既能避免文档大小限制,也有助于分页。您还可以通过在 blog_id 上创建索引来加速此方案,以便快速获取某篇博文的评论。关于索引的更多信息可参见 MongoDB 文档 。
混合搭配,灵活运用……
在分桶模式下,评论文档可能如下所示:
{
"_id" : "comment-bucket-1" ,
"blog_id" : "blog-1" ,
"comments" : [
{
"username" : "Anonymous User" ,
"content" : "Hey great article, it really helped a lot!"
} ,
] ,
"comment_count" : 57
} 技术支持
在此场景中,当创建新评论时,您查询尚未满额的桶;将新评论推入数组并增加计数值。虽然这种策略在维护分桶时更依赖应用层逻辑,但会带来更好的性能。如果您的博客采用了无限滚动机制,那就更好了,因为您可以借助分桶更好地实现分页。
尽管超出了本文的范围,但如果您想了解添加新评论的大致过程,您需要在评论桶集合中过滤(见下面的示例查询),找到 blog_id 匹配且 comment_count 小于您设定值的桶。
在匹配的基础上,您将在更新条件中使用 $push 操作符,并使用 $inc 操作符增加评论计数字段的值。所有这一切都可以在一次操作中完成;如果使用 upsert 标志,当不存在符合条件的评论桶时,还可以创建一个新的。
更新查询示例:
db.commentBuckets.updateOne(
{ blog_id: "blog-1" , comment_count: { $lt: 100 } } ,
{ $push: { comments: newComment } , $inc: { comment_count: 1 } } ,
{ upsert: true }
)技术支持
结语
尽管我们很想说应将有关博客的一切都存储在一个嵌套文档中,但这大概并不是个好主意。就像采用一个适合关系型数据库却不一定适合像 MongoDB 这样的文档数据库的数据模型,也可能不是好主意。
在 MongoDB 中,您应尽可能减少引用关系,以获得尽可能好的性能。
在扩展时请牢记以下几点:
如果预计评论很多,请考虑使用分桶模式,或将其与在博文中部分嵌入的方式结合使用。
您的分页需求是什么?
根据应用中的查询方式为集合建立索引。
再次强调,请思考您将在前端如何展示博客数据。您是否真的需要在每次请求中加载全部作者信息,还是只需要其中一部分?您是否真的要在加载博文时一次性加载一百万条评论,还是只加载少量即可?当您认真思考这些问题时,您会惊讶于其实有多少联接操作并非必要。
如果您是 MongoDB 新手,建议查看Python 中的 MongoDB 入门课程 。
想要构建您自己的内容管理系统(CMS),也就是我们常说的博客吗?这是学习如何使用数据库时的经典案例,无论是关系型数据库管理系统(RDBMS)还是 NoSQL 数据库,因为它既涉及潜在的大量数据,也涉及数据之间的关系。博客应用的示例也非常适用于其他数据建模需求。
本文将探讨在 MongoDB 中设计 NoSQL 文档时的注意事项。不过我们不会真正开发一个博客应用,而是仅从数据的角度进行分析。
博客应用的组成部分
在尝试为博客设计文档模型之前,最好先退一步,思考每个可能与数据相关的组件。
一个典型的博客可能具有以下特性:
当然,根据您的业务需求,功能列表可能会更长、更复杂。为保持简单,我们将基于上述内容来建模。
可行但不推荐的做法
看到上述特性后,您的第一反应可能是将每个条目视为一个独立的文档。这是您在 Postgres 等关系型数据库中会采用的典型方法。在 MongoDB 中,它可能看起来像下面这样:
以上是作者的一个大幅精简表示。当然,您还可以有许多其他字段,如
email等等,但这在这里并不重要。接着,我们可能会为博文设计如下结构:
与前一个类似,博客文档还可以有其他字段,如
tags等您能想到的内容。这里的重点是两个文档之间通过id进行关联。稍后我们会继续讨论。该场景中的最后一个文档模型是评论:
同样,在该场景中,评论文档与博客文档之间基于
id值建立引用关系。这在关系型数据库中非常常见,实际上在 MongoDB 中也能正常工作。然而,这并不能带来最佳的 MongoDB 体验。不建议在 MongoDB 中这样做的一个原因是性能问题。
要连接这三个文档,您需要在聚合管道中使用
$lookup操作。$lookup并不便宜,随着数据库规模扩大,它会对性能产生负面影响,尤其是当管道中有多个$lookup操作时。在 MongoDB 中,有更好的解决方案。
更优、更符合 MongoDB 思维的数据建模方式
MongoDB 的一条基本规则是:一起被访问的数据应尽量存放在一起。将每个功能拆分到独立的文档或集合会违背这条规则,因为向用户展示的数据不再能通过一次获取完成。
让我们换个思路,为博客建模如下:
好的,上述模型满足了 MongoDB 的一条主要规则。我们在一个文档中使用嵌套来把该作者的所有博文与评论放在一起。
虽然可行,但把博客应用的文档这样建模可能并不是好主意。
MongoDB 存在文档大小限制,而当文档中包含无界数组时,您有可能触及这些限制。更不用说数组变大后可能带来的性能问题。在此例中,我们可能有无限数量的博文以及每篇博文无限数量的评论。某个时刻会变得难以维护。
在这个示例中,更好的方法可能是将两种策略加以组合。
请记住,作者、博文和评论都可能有任意数量的字段。在本例中,我们保持了最小化。考虑到这一点,我们知道最常被访问的数据将是博文本身。来看一下新的设计:
本例中的第二个文档仍然是作者信息的原始文档:
我们知道在展示一篇博文时应包含部分作者信息,但不一定需要全部信息。我们可以在博文中嵌入所需的作者信息以及
_id,如果用户想了解更多作者信息,可以在您的应用中进一步查看。这样就不必每次都进行$lookup操作,从而提升性能。现在,我们面临的问题是:某篇博文可能存在无限数量的评论,而这些评论目前是以数组形式附加的。
有几种处理方法,最终取决于您的选择:
$lookup将其包含进来。blog_id上创建索引来加速此方案,以便快速获取某篇博文的评论。关于索引的更多信息可参见 MongoDB 文档。在分桶模式下,评论文档可能如下所示:
在此场景中,当创建新评论时,您查询尚未满额的桶;将新评论推入数组并增加计数值。虽然这种策略在维护分桶时更依赖应用层逻辑,但会带来更好的性能。如果您的博客采用了无限滚动机制,那就更好了,因为您可以借助分桶更好地实现分页。
尽管超出了本文的范围,但如果您想了解添加新评论的大致过程,您需要在评论桶集合中过滤(见下面的示例查询),找到
blog_id匹配且comment_count小于您设定值的桶。在匹配的基础上,您将在更新条件中使用
$push操作符,并使用$inc操作符增加评论计数字段的值。所有这一切都可以在一次操作中完成;如果使用upsert标志,当不存在符合条件的评论桶时,还可以创建一个新的。更新查询示例:
结语
尽管我们很想说应将有关博客的一切都存储在一个嵌套文档中,但这大概并不是个好主意。就像采用一个适合关系型数据库却不一定适合像 MongoDB 这样的文档数据库的数据模型,也可能不是好主意。
在 MongoDB 中,您应尽可能减少引用关系,以获得尽可能好的性能。
在扩展时请牢记以下几点:
再次强调,请思考您将在前端如何展示博客数据。您是否真的需要在每次请求中加载全部作者信息,还是只需要其中一部分?您是否真的要在加载博文时一次性加载一百万条评论,还是只加载少量即可?当您认真思考这些问题时,您会惊讶于其实有多少联接操作并非必要。
如果您是 MongoDB 新手,建议查看Python 中的 MongoDB 入门课程。