跳至主要内容

面向博客应用的 MongoDB 数据建模指南

在设计内容管理系统(CMS)或博客应用时,了解一些包含嵌套文档的数据建模可能性。
· 7分钟 阅读

使用 AI 探索

想要构建您自己的内容管理系统(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!"
		},
		// More comments here...
	],
	"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 入门课程。

FAQs

我可以将博客应用的数据模型从关系型数据库迁移到 MongoDB 吗?

您可以创建带有 id 引用的扁平文档,也能运行,但无法发挥 MongoDB 的最佳性能。

为什么不把所有内容都放在一个文档里?

MongoDB 存在大小限制,而当您使用无界数组时,很容易触及这些限制。

文中提到了分桶模式,还有其他模式吗?

还有其他可能适用的模式,但在处理大型数组时,分桶模式是一个不错的选择。

MongoDB 的文档大小限制是多少?

MongoDB 文档的最大大小为 16MB。

如何处理嵌套数据的更新(例如编辑评论)?

对于嵌入数据,您可以使用诸如 $set 等更新操作符;对于分桶的评论,您可以定位到对应的桶并更新其中的特定评论。

Nic Raboy

作者

Nic Raboy

Nic Raboy 是 MongoDB 的开发者关系主管,带领一支由 Python、Java、C# 与 PHP 开发者组成的团队,创作优质内容,帮助开发者在项目中成功使用 MongoDB。他具备 Golang 与 JavaScript 经验,并常常撰写自己在开发过程中的见闻与实践。

主题