问题
正如之前所述,过度嵌入会导致几个问题:
问题1:BSON大小限制
截至本文撰写时,BSON文档限制为16MB。如果达到该限制,MongoDB将会抛出异常并且您无法再添加更多的评论,在最坏的情况下,如果更改会增加文档的大小,则甚至无法更改(用户)名称或图片。
问题2:查询限制和性能
在某些情况下,不容易查询或对评论数组进行排序。有些事情需要相当昂贵的聚合,其他的则是相当复杂的语句。
虽然可以说一旦查询就位,这不是什么问题,但我不同意。首先,查询越复杂,优化对于开发人员以及后续的MongoDB查询优化器来说就越困难。例如,我曾经通过简化数据模型和查询,使响应速度提高100倍。
在扩展时,为了处理复杂和/或昂贵的查询所需的资源可能甚至会相当于整个机器,与简单数据模型和相关查询相比。
问题3:可维护性
最后,您可能会遇到维护代码的问题。作为一个简单的经验法则:
代码变得越复杂,维护就越困难。维护代码需要更多时间。维护代码所需的时间越长,花费就会越高。
结论:复杂的代码很昂贵。
在这种情况下,“昂贵”既指金钱(适用于专业项目)也指时间(适用于兴趣项目)。
(我的!)解决方案
很简单:简化您的数据模型。因此,您的查询将变得不那么复杂,(希望)更快。
步骤1:确定您的使用情况
对我来说,这将是一个猜测,但重要的是要向您展示一般方法。我将定义您的使用情况如下:
- 对于给定的帖子,用户应该能够发表评论
- 对于给定的帖子,显示作者和评论以及评论者和作者的用户名和头像
- 对于给定的用户,应该很容易更改名称、用户名和图片
步骤2:相应地建模
用户
首先,我们有一个简单的用户模型
{
_id: new ObjectId(),
name: "Joe Average",
username: "HotGrrrl96",
picture: "some_link"
}
这里没有新内容,仅为完整性而添加。
帖子
{
_id: new ObjectId()
title: "A post",
content: " Interesting stuff",
picture: "some_link",
created: new ISODate(),
author: {
username: "HotGrrrl96",
picture: "some_link"
}
}
这就是一篇文章的全部内容。需要注意两点:首先,我们在显示文章时立即存储所需的作者数据,因为这样可以节省查询非常常见(甚至无处不在)的用例。那么为什么不将评论和评论者的数据也保存起来呢?因为考虑到16 MB的大小限制,我们试图避免在单个文档中存储参考数据。相反,我们将参考数据存储在评论文档中:
评论
{
_id: new ObjectId(),
post: someObjectId,
created: new ISODate(),
commenter: {
username: "FooBar",
picture: "some_link"
},
comment: "Awesome!"
}
与帖子一样,我们拥有显示帖子所需的所有必要数据。
查询
现在我们已经规避了BSON大小限制,并且不需要引用用户数据就能够显示帖子和评论,这应该可以节省我们很多查询。但让我们回到用例和更多的查询。
添加评论
现在完全简单明了。
获取给定帖子的所有或某些评论
对于所有评论:
db.comments.find({post:objectIdOfPost})
最近3条评论
db.comments.find({post:objectIdOfPost}).sort({created:-1}).limit(3)
因此,为了显示一篇帖子及其所有(或某些)评论,包括用户名和图片,我们需要进行两个查询。比之前所需的要多,但我们绕过了大小限制,基本上您可以针对每个帖子拥有无限数量的评论。但是让我们来看看真正的内容。
获取最新的5篇文章及其最新的3条评论
这是一个两步过程。但是,通过适当的索引(稍后将回到此处),这仍然应该很快(因此节省资源):
var posts = db.posts.find().sort({created:-1}).limit(5)
posts.forEach(
function(post) {
doSomethingWith(post);
var comments = db.comments.find({"post":post._id}).sort("created":-1).limit(3);
doSomethingElseWith(comments);
}
)
获取特定用户的所有帖子按时间从新到旧排序以及它们的评论
var posts = db.posts.find({"author.username": "HotGrrrl96"},{_id:1}).sort({"created":-1});
var postIds = [];
posts.forEach(
function(post){
postIds.push(post._id);
}
)
var comments = db.comments.find({post: {$in: postIds}}).sort({post:1, created:-1});
注意这里只有两个查询。虽然你需要“手动”连接帖子和它们各自的评论,但这应该非常简单。
更改用户名
这可能是一个很少使用的用例。然而,使用上述数据模型并不是很复杂。
首先,我们更改用户文档。
db.users.update(
{ username: "HotGrrrl96"},
{
$set: { username: "Joe Cool"},
$push: {oldUsernames: "HotGrrrl96" }
},
{
writeConcern: {w: "majority"}
}
);
我们将旧用户名推送到相应的数组中。这是一种安全措施,以防以下操作出现问题。此外,我们将写入关注度设置为相当高的水平,以确保数据持久。
db.posts.update(
{ "author.username": "HotGrrrl96"},
{ $set:{ "author.username": "Joe Cool"} },
{
multi:true,
writeConcern: {w:"majority"}
}
)
这里没有什么特别的。评论的更新语句看起来几乎一样。尽管这些查询需要一些时间,但它们很少执行。
索引
可以说 MongoDB 每个查询只能使用一个索引。虽然这不完全正确,因为有索引交集,但很容易处理。另外,复合索引中的单独字段可以独立使用。因此,索引优化的简单方法是找到在使用索引的操作中使用最多字段的查询,并创建一个复合索引。请注意查询中出现的顺序很重要。所以,让我们开始吧。
帖子
db.posts.createIndex({"author.username":1,"created":-1})
评论
db.comments.createIndex({"post":1, "created":-1})
结论
每个帖子完全嵌入一个文档确实是加载它及其评论最快的方法。然而,它不具有良好的可扩展性,并且由于可能需要处理它的复杂查询的性质,这种性能优势可能会被利用或甚至消除。
通过上述解决方案,您可以在某些情况下(如果!)交换一些速度,以获得基本无限的可扩展性和更简单的数据处理方式。
希望对您有所帮助。