聚合管道中过滤数组为空时保留主文档

Keep the main document when filtered array is empty in aggregation pipeline

这真是一个的补充。在@JohnnyHK 的帮助下,我现在可以根据特定条件从数组中删除不需要的子文档:deleted != null。我发现当数组中没有项目时我的 $unwind 管道坏了,所以我还实现了一个 $cond 来添加一个数组为空的虚拟对象。这是到目前为止的代码:

Collection.aggregate([
        { $match:
            { _id: ObjectID(collection_id) }
        },
        {
          $project:
            {
              _id: 1,
              name: 1,
              images: {
                $cond:
                  [
                    {
                      $eq:
                        [
                          "$images",
                          []
                        ]
                    },
                    [
                      dummyImg // Variable containing dummy object
                    ],
                    '$images'
                  ]
              },
            }
        },
        { $unwind: "$images" },
        { $match:
            { "images.deleted": null }
        },

        // Regroup the docs by _id to reassemble the images array
        {$group: {
            _id: '$_id',
            name: {$first: '$name'},
            images: {$push: '$images'}
        }}

    ], function (err, result) {
    if (err) {
        console.log(err);
        return;
    }
    console.log(result);
});

当数组不为空但仅包含 deleted 为 null 的对象时,现在会出现问题。我 $unwind 图像,但 $match 没有找到任何匹配项,因此无法执行最后的 $group

我正在考虑在管道的开头推入虚拟对象,然后在结束时对图像进行计数。如果虚拟对象是唯一的,它将保留下来,但如果有其他图像对象已经通过管道,则需要将其删除。

如果这是一条明智的路线,我会很高兴得到一些指示。如果我的想法偏离了方向,我们将不胜感激地收到任何引导我朝着正确方向前进的提示。

谢谢。

现代 MongoDB 版本当然只是应用 $filter and $addFields 将过滤后的可能为空的数组结果写入文档:

Collection.aggregate([
  { "$addFields": {
    "images": {
      "$filter": {
        "input": "$filter",
        "as": "i",
        "cond": { "$eq": [ "$$i.deleted", null ] }
      }
    }
  }}
])

你之前得到的答案不是一个非常现代或有效的答案,因为有其他更好的方法来处理过滤数组中的内容,而不是 $unwind$match$group.

与 MongoDB 2.6 一样,您可以使用唯一的数组标识符来做到这一点:

Collection.aggregate([
    { "$project": {
        "name": 1,
        "images": { "$cond": [
            { "$eq": [{ "$size": { "$ifNull": [ "$images",[]] }}, 0] },
            { "$ifNull": [ "$images", [] ] },
            { "$setDifference": [
                { "$map": {
                    "input": "$images",
                    "as": "i",
                    "in": { "$cond": [
                        { "$eq": [ "$$i.deleted", null ] },
                        "$$i",
                        false
                    ]}
                }},
                [false]
            ]}
        ]}
    }}
],

$map operator transforms arrays in place in the document by returning each inspected element evaluated by the given expression. Here you can use $cond 为了测试字段值并决定是否 return 字段原样或其他 return false.

$setDifference 操作 "compares" 生成的变换数组与另一个奇异元素数组 [false]。这具有从数组中删除所有不匹配项的效果,并只留下那些项,甚至留下一个没有匹配项的空数组。


只要您的文档不在文档的多个级别包含相同的引用 属性,以下带有 $redact 的内容就是安全的。看起来有些滑稽的情况是因为 "deleted": null 属性 实际上被投影(用于评估目的)在它不存在的级别。这是必需的,因为 $redact 以 "recursive" 的方式使用,下降文档树以决定它摆脱什么,或者 "redacts":

Collection.aggregate([
    { "$redact": {
        "$cond": [ 
            { "$eq": [ { "$ifNull": [ "$deleted", null ] }, null ] },
            "$$DESCEND",
            "$$PRUNE"
        ]
    }}
]

这确实是为您的特定目的而实施的最简单的逻辑。请记住,如果您在文档中添加另一个 "deleted" 字段以某种方式表示其他含义,您以后可能无法使用它。


如果你真的被 2.6 之前的 MongoDB 版本所困,并且无法访问这些操作,那么你当然需要执行 $unwind$match$group 处理。因此,在开始时需要注意空数组或缺失数组,以及匹配没有匹配项的数组时。

一种方法:

Collection.aggregate([
    // Cater for missing or empty arrays
    { "$project": {
        "name": 1,
        "images": { "$cond": [
            { "$eq": [{ "$ifNull": [ "$images", [] ] }, [] ] },
            { "$const": [{ "deleted": false }] },
            "$images"
        ]}
    }},

    // Safe to unwind
    { "$unwind": "$images" },

    // Just count the matched array entries first
    { "$group": {
        "_id": "$_id",
        "name": { "$first": "$name" },
        "images": { "$push": "$images" },
        "count": { "$sum": { "$cond": [
            { "$eq": [ "$images.deleted", null ] },
            1,
            0
        ]}}
    }},

    // Unwind again
    { "$unwind": "$images" },

    // Match either non deleted or unmatched array
    { "$match": { 
        "$or": [
            { "images.deleted": null},
            {  "count": 0 }
        ]
    }},

    // Group back with the things that were matched
    { "$group": {
        "_id": "$_id",
        "name": { "$first": "$name" },
        "images": { "$push": "$images" },
        "count": { "$first": "$count" }
    }},

    // Replace the un-matched arrays with empty ones
    { "$project": {
         "name": 1,
         "images": { "$cond": [
             { "$eq": [ "$count", 0 ] },
             [],
             "$images"
         ]}
    }}
],

所以那里有更多的提升,但一般原则是只获得 "count" 匹配元素,当你过滤时,你还保留数组项 0 "count" 但稍后只需替换整个数组。

您还可以在这里考虑,如果您首先在文档中维护 "activeCount" 字段,那么您将不需要计算它并删除几个阶段。

当然,这里的另一个论点是,您可以通过在单独的数组中实际维护 "active" 和 "deleted" 项来省去这方面的麻烦。每次更新都这样做可以消除通过聚合进行过滤的任何需要。我想这完全取决于您的真实目的。但深思。


当然这都是根据您的原始数据进行的测试,并进行了一些修改以适应测试用例:

{
    "_id" : ObjectId("54ec9cac83a214491d2110f4"),
    "name" : "my_images",
    "images" : [
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2311026b0cb289ed04188"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:20:16.961Z")
        },
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2314a26b0cb289ed04189"),
            "deleted" : ISODate("2015-02-24T15:38:14.826Z"),
            "date_added" : ISODate("2015-02-28T21:21:14.910Z")
        },
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315526b0cb289ed0418a"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:25.042Z")
        },
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315d26b0cb289ed0418b"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:33.081Z")
        }
    ]
},
{
    "_id" : ObjectId("54fa6ca87c105bc872cc1886"),
    "name" : "another",
    "images" : [ ]
},
{
    "_id" : ObjectId("54fa6cef7c105bc872cc1887"),
    "name" : "final",
    "images" : [
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2314a26b0cb289ed04189"),
            "deleted" : ISODate("2015-02-24T15:38:14.826Z"),
            "date_added" : ISODate("2015-02-28T21:21:14.910Z")
        }
    ]
}

所有版本都产生安全结果:

{
    "_id" : ObjectId("54ec9cac83a214491d2110f4"),
    "name" : "my_images",
    "images" : [
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2311026b0cb289ed04188"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:20:16.961Z")
        },
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315526b0cb289ed0418a"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:25.042Z")
        },
        {
            "ext" : "jpeg",
            "type" : "image/jpeg",
            "_id" : ObjectId("54f2315d26b0cb289ed0418b"),
            "deleted" : null,
            "date_added" : ISODate("2015-02-28T21:21:33.081Z")
        }
    ]
},
{
    "_id" : ObjectId("54fa6ca87c105bc872cc1886"),
    "name" : "another",
    "images" : [ ]
},
{
    "_id" : ObjectId("54fa6cef7c105bc872cc1887"),
    "name" : "final",
    "images" : [ ]
}