【问题标题】:Aggregate/project sub-document as top-level document in mongo聚合/项目子文档作为 mongo 中的顶级文档
【发布时间】:2017-02-19 11:53:57
【问题描述】:

在我的一个集合中执行了几个聚合步骤(管道步骤)后,我得到了以下结果:

{
    "_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

【问题讨论】:

    标签: mongodb mongodb-query


    【解决方案1】:

    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 仅用于删除字段时的替代语法。

    【讨论】:

      【解决方案2】:

      如您所料,$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 应该产生你正在寻找的结果。

      cmets 后更新

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

      aggregate 的产品只允许您排除 _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 多个字段。我应该像上面那样投影每个字段吗?有没有其他方法可以投影所有字段但只跳过 1 个字段?
      • 我相信聚合只会让你隐藏_id。否则,逻辑仅适用于“要显示的字段列表”。您也可以使用 $out 重定向结果,然后使用 find(),它的工作方式相反:它允许您列出要隐藏的字段..
      • 我用建议更新了答案(“cmets 后更新”部分)
      • 由于 MongoDB 3.4 $project 可以排除字段:docs.mongodb.com/manual/reference/operator/aggregation/project/…
      【解决方案3】:

      如果子文档中有很多很多字段,并且偶尔会使用新字段进行更新,那么投影就不是一个可行的选择。幸运的是,从 3.4 开始,MongoDB 有了一个名为 $replaceRoot 的新运算符。

      您所要做的就是在管道末端添加一个新阶段。

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

      这会给你想要的输出。

      请注意,在聚合的情况下(尤其是在 $group 阶段之后)“命令”文档可以是一个数组,并且可以包含多个文档。在这种情况下,您需要先 $unwind 数组才能使用 $replaceRoot

      【讨论】:

        猜你喜欢
        • 2013-10-26
        • 1970-01-01
        • 2018-01-22
        • 1970-01-01
        • 2021-09-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-30
        相关资源
        最近更新 更多