在Mongo中将聚合/项目子文档作为顶级文档汇总/投射

17

在我的一个集合中进行了几个聚合步骤后,最终结果如下:

{
    "_id" : ObjectId("574e7722bffe901713d383bb"),
    "eventname" : "Ball Passed",
    "command" : {
        "_id" : ObjectId("57ec6b6f6c61e919b578fe7c"),
        "name" : "Run",
        "strike" : 15,
        "score" : true,
        "duration" : 123
    }
}
{
    "_id" : ObjectId("57ec6b6f6c61e919b578ff8a"),
    "eventname" : "Ball Passed",
    "command" : {
        "_id" : ObjectId("573d688d080cc2cbe8aecbbc"),
        "name" : "Run",
        "strike" : 12,
        "score" : false,
        "duration" : 597
    }
}

没问题!

但是,在聚合的下一步中,我想获得以下结果:

{
    "_id" : ObjectId("57ec6b6f6c61e919b578fe7c"),
    "name" : "Run",
    "strike" : 15,
    "duration" : 123
}
{
    "_id" : ObjectId("573d688d080cc2cbe8aecbbc"),
    "name" : "Run",
    "strike" : 12,
    "duration" : 597
}
如果您注意到了,command 字段应该成为顶层文档,command.score 应该被跳过。
我该如何在一步中实现此操作?如果无法在一步中实现,则需要多步吗?我猜我得使用 $project
3个回答

26

如果子文档中有很多字段,并且偶尔会更新新字段,那么投影不是可行的选项。幸运的是,自3.4以来,MongoDB有一个名为$replaceRoot的新操作符。

你所需要做的就是在管道的末尾添加一个新的阶段。

db.getCollection('sample').aggregate([
    {
        $replaceRoot: {newRoot: "$command"}
    },
    {
        $project: {score: 0 } //exclude score field
    }
])

这将为您提供所需的输出结果。

请注意,在聚合时(特别是在$group阶段之后),'command'文档可能是一个数组并且可能包含多个文档。在这种情况下,您需要首先对该数组进行$unwind操作,才能使用$replaceRoot


14

正如您所猜测的那样,$project 允许您这样做:

db.col.aggregate([
{
    $project : 
    {
        _id: "$command._id",
        name: "$command.name", 
        strike: "$command.strike", 
        duration: "$command.duration"
    }
}
]).pretty()

我插入了您之前的结果,上述查询返回了以下结果:

{
    "_id" : ObjectId("57ec6b6f6c61e919b578fe7c"),
    "name" : "Run",
    "strike" : 15,
    "duration" : 123
}
{
    "_id" : ObjectId("573d688d080cc2cbe8aecbbc"),
    "name" : "Run",
    "strike" : 12,
    "duration" : 597
}

使用$product来处理你的查询,应该可以产生你要寻找的结果。

评论后更新

如果确切的结构不是你关心的主要问题,而是要排除一些字段(而不必列出所有要包含的字段),则可以使用find()而不是aggregate()

aggregate的product仅允许您排除_id。这意味着您需要手动列出要包含的所有字段。
注意:自MongoDB 3.4版本以来,可以在$project阶段中排除字段(https://docs.mongodb.com/manual/reference/operator/aggregation/project/#exclude-fields

find允许您列出要隐藏的字段。

替代方案

(1) 您可以使用$out将聚合结果重定向到另一个集合:

{ $out : "commands" }

(2) 即使结构不完全符合您的要求,您仍然能够执行find查询并隐藏字段:

db.commands.find({}, {_id:0, "command.score":0, eventname:0}).pretty()
它会返回这个结果,与你所期望的非常接近:
{
    "command" : {
        "_id" : ObjectId("57ec6b6f6c61e919b578fe7c"),
        "name" : "Run",
        "strike" : 15,
        "duration" : 123
    }
}

在这个问题中,我已经简化了我的文档。实际上,在“command”子文档中有超过70个字段。我是否应该像上面那样投影每个字段?是否有其他方法可以投影所有字段但跳过一个字段? - Zafar
我相信 aggregate 只能隐藏 _id。否则,逻辑只适用于“要显示的字段列表”。你也可以使用 $out 重定向结果,然后使用 find(),它的作用相反:它允许您列出要隐藏的字段。 - alexbt
我已经更新了答案(“评论后更新”部分)并提出了建议。 - alexbt
1
自MongoDB 3.4起,$project可以排除字段:https://docs.mongodb.com/manual/reference/operator/aggregation/project/#exclude-fields - SteveB

0

Mongo 4.2 开始,$replaceWith 聚合操作符可用于将一个文档(在我们的情况中是子文档)替换为另一个文档作为语法糖 $replaceRoot

// { "eventname": "Ball Passed", "command": { "_id": "57e...", "name": "Run", "strike": 15, "score": true,  "duration": 123 } }
// { "eventname": "Ball Passed", "command": { "_id": "573...", "name": "Run", "strike": 12, "score": false, "duration": 597 } }
db.collection.aggregate([
  { $replaceWith: "$command" }, // replaces the document by the content of "command"
  { $unset: ["score"] }         // drops the "score" field
])
// { "_id" : "57e...", "name" : "Run", "strike" : 15, "duration" : 123 }
// { "_id" : "573...", "name" : "Run", "strike" : 12, "duration" : 597 }

还要注意在Mongo 4.2中引入的$unset聚合操作符,作为$project的替代语法,仅用于删除字段。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接