MongoDB的聚合框架:仅投影数组中匹配的元素

6

I have a "class" document as:

{
className: "AAA",
students: [
   {name:"An", age:"13"},
   {name:"Hao", age:"13"},
   {name:"John", age:"14"},
   {name:"Hung", age:"12"}
   ]
}

我希望能够获取名字为“An”的学生,只在数组“students”中匹配元素。我可以使用find()函数来完成:

>db.class.find({"students.name":"An"}, {"students.$":true})
{
"_id" : ObjectId("548b01815a06570735b946c1"),
"students" : [ 
    {
        "name" : "An",
        "age" : "13"
    }
]}

这个没问题,但是当我按照以下聚合的方式操作时,出现了错误:

db.class.aggregate([
   {$match:{"students.name":'An'}},
   {$project:{"students.$":true}}
])

错误信息如下:

uncaught exception: aggregate failed: {
    "errmsg" : "exception: FieldPath field names may not start with '$'.",
    "code" : 16410,
    "ok" : 0
}

为什么我不能在aggregate()的$project操作符中使用"$"来表示数组,而在find()的project操作符中可以使用?
3个回答

3

来自文档

在查询方法find()或findOne()的投影文档中使用$,当您只需要选定文档中的一个特定数组元素时。

聚合管道投影阶段中不能使用位置操作符$。它在那里不被识别。

这是有道理的,因为当您执行查找查询时,与查询匹配的单个文档将作为查询的投影部分的输入。即使在投影期间,匹配的上下文也是已知的。因此,对于每个匹配查询的文档,在找到下一个匹配之前,投影运算符会立即应用。

db.class.find({"students.name":"An"}, {"students.$":true})

在以下情况下:

db.class.aggregate([
   {$match:{"students.name":'An'}},
   {$project:{"students.$":true}}
])
聚合管道是由一系列阶段组成的。每个阶段都完全不知道其前一个或后一个阶段的存在并且相互独立。一组文档在传递到管道中的下一个阶段之前必须完全通过当前阶段。在本例中,第一个阶段是$match阶段,所有文档都基于匹配条件进行筛选。投影阶段的输入现在是作为匹配阶段的一部分已被过滤的文档集合
因此,在投影阶段中使用位置运算符没有意义,因为在当前阶段它不知道字段的过滤基础是什么。因此,$运算符不能作为字段路径的一部分。
为什么以下内容有效?
db.class.aggregate([
     { $match: { "students.name": "An" },
     { $unwind: "$students" },
     { $project: { "students": 1 } }
])

正如您所见,投影阶段将一组文档作为输入,并投影所需字段。它独立于其前后阶段。

谢谢!你帮我理解了为什么在aggregate()的$project操作符中不能使用"$"来表示数组,而在find()的project操作符中可以使用。我会在我的项目中采用这个解决方案,但我还发现了另一种不需要展开数组的方法。如果数组中有很多元素,性能会很低。无论如何...再次感谢你! - Deka
1
是的,你说得对。如果你知道并且期望查询只有一个匹配子文档,那么你应该像你的问题中一样盲目地使用find()查询。但不幸的是,在聚合中,你只能通过展开(unwinding)来实现。 - BatScream

1

我之前尝试过,但我认为这个解决方案不好。 当我们使用$unwind时,文档会被克隆很多次(与数组中的元素数量相同),可能会影响性能,我想投影它而不是展开它。 - Deka
有道理。那么你可以在管道中交换unwind和match的顺序,以便先过滤文档,然后展开以便更容易进行投影。请参见更新的答案。 - atyagi
这也许是个可接受的解决方案,但我不知道为什么在aggregate()的$project运算符中无法使用"$"来表示数组,而在find()的project运算符中可以使用。 - Deka

1
你可以使用 $filter 来选择数组的子集,根据指定条件返回结果。
db.class.aggregate([
   {
       $match:{
          "className": "AAA"
       }
   },
   {
       $project: {
          $filter: {
             input: "$students",
             as: "stu",
             cond: { $eq: [ "$$stu.name", "An" ] }
          }
   }
])

以下示例将学生数组过滤,仅包括名称等于“An”的文档。

我认为这应该是被接受的答案, 只是注意@jitendra className key缺少一个闭引号。 - mosid

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