这实际上有几种方法可供考虑,每种方法都有其自身的优点或缺点。
嵌入
最清晰和最简单的方法是将“菜单”和“数量”直接嵌入餐厅父文档中。
这实际上也很合理,因为您似乎被困在关系建模术语中,而MongoDB不是关系型数据库管理系统,通常也不应该作为一个。相反,我们发挥MongoDB的优势。
结构将如下所示:
{
_id: "100",
location: { coordinates: [ -63, 42 ], type: "Point" },
name: "Burger King",
menuCount: 1,
menu: [
{
name: "Fried Chicken",
price: 12.99
}
]
}
实际上,这很容易查询,事实上我们可以简单地使用常规的 $nearSphere
应用程序,因为我们真的不需要进一步聚合条件:
restoModel.find({
"location": {
"$nearSphere": {
"$geometry": {
"type": "Point",
"coordinates": coordinates
},
"$maxDistance": 1000
}
},
"menuCount": { "$gt": 1 }
}).skip(0).limit(10)
简单而有效。这正是为什么您应该使用MongoDB的原因,因为“相关”的数据已经嵌入到父项中。当然,这样做存在“权衡”,但最大的优点在于速度和效率。
在父级中维护菜单项以及当前计数也很简单,我们只需在添加新项时“增加”计数即可。
restoModel.update(
{ "_id": id, "menu.name": { "$ne": "Pizza" } },
{
"$push": { "menu": { "name": "Pizza", "price": 19.99 } },
"$inc": { "menuCount": 1 }
}
)
在一个原子操作中,它将新项目添加到尚不存在的位置并增加菜单项的数量,这是将关系嵌入其中的另一个原因,因为更新同时对父级和子级产生影响。这确实是你应该追求的目标。当然,你实际上可以嵌入的东西是有限制的,但这只是一个“菜单”,相对于我们可以定义的其他类型的关系,它当然是相对较小的。MongoDB的Elliot在某个时候曾经说过最好的话:“整个《战争与和平》的文本内容都适合4MB”,那时BSON文档的限制是4MB。现在是16MB,足以处理大多数客户可能需要浏览的任何“菜单”。
使用 $lookup 进行聚合
如果您遵循标准的关系型模式,就会遇到一些问题。最大的区别在于“嵌入”和“菜单”数据位于另一个集合中,因此您需要使用 $lookup
将其“提取”,然后“计数”。
与“最近”的查询相关的是,不像上面的示例,我们不能将这些额外的限制条件 “放在‘near’查询本身内部”,这意味着在 $geoNear
返回的默认 100 个结果中,有些项目“可能不符合”附加约束条件,您只能在 $lookup
执行之后“应用”它们:
restoModel.aggregate([
{ "$geoNear": {
"near": {
"type": "Point",
"coordinates": coordinates
},
"spherical": true,
"limit": 150,
"distanceField": "distance",
"maxDistance": 1000
}},
{ "$lookup": {
"from": "menuitems",
"localField": "_id",
"foreignField": "resto_id",
"as": "menu"
}},
{ "$redact": {
"$cond": {
"if": { "$gt": [ { "$size": "$menu" }, 0 ] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$limit": 10 }
])
因此,在这里您唯一的选择是“增加”“可能”的返回数量,然后执行其他管道阶段以“连接”,“计算”和“过滤”。同时将最终的
$limit
留给它自己的管道阶段。
一个显著的问题在于结果的“分页”。这是因为“下一页”需要基本上“跳过”先前页面的结果。为此,最好实现“向前翻页”的概念,就像在这篇文章中所描述的那样:
在MongoDB中实现分页
总体思路是通过
$nin
“排除”之前“查看”的结果。这实际上是可以使用
$geoNear
的“查询”选项来完成的:
restoModel.aggregate([
{ "$geoNear": {
"near": {
"type": "Point",
"coordinates": coordinates
},
"spherical": true,
"limit": 150,
"distanceField": "distance",
"maxDistance": 1000,
"query": { "_id": { "$nin": list_of_seen_ids } }
}},
{ "$lookup": {
"from": "menuitems",
"localField": "_id",
"foreignField": "resto_id",
"as": "menu"
}},
{ "$redact": {
"$cond": {
"if": { "$gt": [ { "$size": "$menu" }, 0 ] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$limit": 10 }
])
至少这样做可以避免得到与上一页相同的结果。但这需要更多的工作量,比之前展示的嵌入模型所需的工作量要大得多。
结论
一般情况下,"嵌入式"是这种情况的更好选择。您有一个“小”的相关项,数据实际上与父项直接关联,通常您希望同时获取菜单和餐厅信息。
MongoDB自3.4以来的现代版本确实允许创建"视图", 但一般原则基于使用聚合管道。因此,我们可以在“视图”中“预连接”数据,但由于任何查询操作实际上都会获取底层聚合管道语句进行处理,因此无法应用$nearSphere
等标准查询运算符,因为标准查询实际上是“附加”到定义的管道中。同样,您也不能使用$geoNear
与“视图”。
也许将来约束条件会改变,但现在由于我们无法对“预连接”的源使用更具关系型设计的必需查询,所以这不可行作为选项。因此,您可以基本上采用其中任一种方式进行操作,但我个人认为最好是像这里嵌入式建模。
$geoNear
将“distanceField”有效地“投影”到最接近查询点的搜索结果中。它还具有“query”选项,其中给定的条件可以在返回的“限制”结果中考虑。然而,您所询问的可能还需要$lookup
,因此上下文在这里很重要。如果您实际上拥有 MongoDB 3.4,则还有另一个好处/可能性。因此,请详细说明您的问题中的细节。请提供一些小样本文档。 - Neil Lunn