【问题标题】:Mongodb: Remove subdocument depending on expression in aggregation pipelineMongodb:根据聚合管道中的表达式删除子文档
【发布时间】:2017-05-24 16:07:03
【问题描述】:

我正在对 mongo 集合执行聚合查询。在管道的特定阶段,如果某个表达式导致 false,我想从结果中删除一个子文档。

这是我希望删除子文档时的查询。

db.getCollection('[module].[virtualwarehouses].supplies').aggregate([
    {
        $match: { 
            $or: [ 
                { artNr: "ART01ds" },
                { GTIN: "GTIN0001" }
            ]
        }
    },
    {
       $lookup: {
           from: '[module].[virtualwarehouses].warehouses',
           localField: 'wId',
           foreignField: '_id',
           as: 'warehouse'
       }
    },
    {
        $unwind: '$warehouse'
    },
    {
        $lookup: {
           from: '[module].[virtualwarehouses].warehouses',
           localField: 'warehouse._id',
           foreignField: 'cIds',
           as: 'warehouseP'
       }
    },
    {
       $unwind: {
            path: '$warehouseP',
            preserveNullAndEmptyArrays: true
        }
    },
    {
        $match: {
            $and: [
                {'warehouse.isDel' : false},
                {$or: [
                    { 'warehouseP.isDel' : false },
                    { 'warehouseP' : { $exists: false } }
                ]},
                {$or: [
                    { 'warehouse.subs' : { $elemMatch: { sKey: "localhost" } } }, 
                    { 'warehouseP.subs' : { $elemMatch: { sKey: "localhost" } } }        
                ]}
            ]
        }
    }
])

------------ RESULT ---------------
{
    "_id" : ObjectId("5922eae4f576274033147127"),
    "GTIN" : "GTIN0001",
    "status" : 0,
    "stock" : 2,
    "wId" : ObjectId("5922e378c4352e2b3ccc7b65"),
    "warehouse" : {
        "_id" : ObjectId("5922e378c4352e2b3ccc7b65"),
        "name" : "Warehouse 2",
        "pId" : "Test Company",
        "type" : 0,
        "source" : 0,
        "cIds" : [],
        "isDel" : false,
        "isEnabled" : true,
        "srcSettings" : {
            "dataSource" : 0,
            "ftpUrl" : "ftps.test.com",
            "ftpDir" : "\\\\serv-s1\\importer",
            "ftpFile" : "test.csv",
            "dropImport" : true
        },
        "subs" : [ 
            {
                "sKey" : "localhost",
                "order" : 500000
            }
        ]
    },
    "warehouseP" : {
        "_id" : ObjectId("5922e441de7c2c0eaca93e9b"),
        "name" : "Warehouse Combo",
        "pId" : "Test Company",
        "type" : 1,
        "source" : 0,
        "cIds" : [ 
            ObjectId("5922e263c4352e2b3ccc7b64"), 
            ObjectId("5922e378c4352e2b3ccc7b65"), 
            ObjectId("5923f49ef5762740331fadd5")
        ],
        "isDel" : false,
        "isEnabled" : true,
        "srcSettings" : null,
        "subs" : [ 
            {
                "sKey" : "fakeSubscriber",
                "order" : 500000
            }
        ]
    }
}

在此查询中,我在集合中查找与文章/GTIN 编号匹配的用品。我做了 2 次查找以获取此可用文章所属的仓库。 (仓库显然可以包含很多物资,因此我们选择单独收集物资,否则会超出文件限制)

我之所以进行 2 次查找是因为在我们的数据模型中,一个仓库可以是多个仓库的组合,所以我需要检查包含这篇文章的仓库是否属于某个仓库组。

现在这就是我的问题出现的地方。人们可以订阅仓库,并且应该只从这些订阅的商店中检索库存信息。订阅存储在 subs 字段中。 (查看上面代码字段中的查询结果)

在上面的查询中,如果订阅字段不包含某个订阅者,我希望删除子文档仓库P(父仓库)。在这种情况下,localhost

到目前为止我尝试过的是:

---- attempt 1, does nothing, always true
    {
        $project: {
            warehouseP: { 
                $cond: {
                    if: {'warehouseP' : { "subs": { sKey: "localhost" } } },
                    then: "$warehouseP",
                    else: null
                }
            }
        }
    }
---- attempt 2, results in 
---- errmsg: FieldPath field names may not contain '.'."
{
    $project: {
        warehouseP: { 
            $cond: {
                if: { 'warehouseP.subs' : { $elemMatch: { sKey: "localhost" } } },
                then: "$warehouseP",
                else: null
            }
        }
    }
}
---- attempt 3, results in:
---- errmsg: Unrecognized expression '$elemMatch'
{
    $project: {
        warehouseP: { 
            $cond: {
                if: { 'warehouseP' : { 'subs' : { $elemMatch: { sKey: "localhost" } } } },
                then: "$warehouseP",
                else: null
            }
        }
    }
}

所以,总结一下。在上面查询的输出中,我希望删除字段warehouseP(将其设置为null),因为它的subs 字段不包含localhost。 (如果包含供应的仓库没有父级,则此字段可能已经为空)我已经尝试了上述方法,但没有任何工作。


编辑,澄清情况。

我有 2 个系列,一个带仓库,一个带耗材。 Data examples 2 个集合。

supplies 集合包含一些简单的对象,其中包含有关文章的一些信息。它还包含对仓库的 ObjectId 引用。

仓库集合包含具有关联数据的仓库。这里的关键是仓库可以是“虚拟的”,这意味着它们只是一组其他仓库。如果是这种情况,他们在字段 cIds(子 ID)中有一个 ObjectId 数组。否则,仓库是一个真正的仓库,它可以在其他集合中有关联的库存。第二个重要元素是 subs 字段。在此字段中,我存储有关谁订阅了此仓库的数据。这里的想法是人们可以订阅某些仓库(谁能订阅和不能订阅哪个仓库的逻辑在这里不相关)。应该只能检索他们订阅的仓库中的物品供应信息。

为了方便起见,我希望有一个查询,如果我提供商品编号/GTIN 和订户密钥,则返回供应信息。如果适用,信息应按“虚拟”仓库分组。我的意思是,如果有人订阅了“虚拟”仓库,他应该会收到如下数据:

---- Warehouses that do not have a parent, all end up in this array
{
    "_id" : null,
    "artNr" : "ART01",
    "GTIN" : null,
    "n" : null,
    "p" : null,
    "source" : null,
    "cIds" : null,
    "warehouses" : [ 
        {
            "_id" : ObjectId("5922e576f576274033145a3f"),
            "n" : "Supplier Warehouse 1",
            "p" : "Bosch",
            "status" : 0,
            "stock" : 5
        }
    ]
}

---- Warehouses that DO have a parent, should be grouped under a document for every 'virtual' (parent) warehouse
{
    "_id" : ObjectId("5922e441de7c2c0eaca93e9b"),
    "artNr" : "ART01",
    "GTIN" : null,
    "n" : "Warehouse Combo",
    "p" : "D Soft",
    "source" : 0,
    "cIds" : [ 
        ObjectId("5922e263c4352e2b3ccc7b64"), 
        ObjectId("5922e378c4352e2b3ccc7b65"), 
        ObjectId("5923f49ef5762740331fadd5")
    ],
    "warehouses" : [ 
        {
            "_id" : ObjectId("5922e263c4352e2b3ccc7b64"),
            "n" : "Warehouse 1",
            "p" : "D Soft",
            "status" : 0,
            "stock" : 5
        }, 
        {
            "_id" : ObjectId("5922e378c4352e2b3ccc7b65"),
            "n" : "Warehouse 2",
            "p" : "D Soft",
            "status" : 0,
            "stock" : 5
        }
    ]
}

{
    *** potentially many other 'virtual' warehouses ***
}

我在上面发布的查询实现了这一点,但在 1 种情况下出错: 如果有人订阅了属于某个组的仓库,则始终显示该组信息。即使订阅者没有订阅那个“虚拟”仓库。

如果我在哪里使用上面的数据作为示例,如果有人说 localhost 订阅了仓库 1,但没有订阅仓库组合,他仍然会收到这样的数据:

{
    "_id" : ObjectId("5922e441de7c2c0eaca93e9b"),
    "artNr" : "ART01",
    "GTIN" : null,
    "n" : "Warehouse Combo",
    "p" : "D Soft",
    "source" : 0,
    "cIds" : [ 
        ObjectId("5922e263c4352e2b3ccc7b64"), 
        ObjectId("5922e378c4352e2b3ccc7b65"), 
        ObjectId("5923f49ef5762740331fadd5")
    ],
    "warehouses" : [ 
        {
            "_id" : ObjectId("5922e263c4352e2b3ccc7b64"),
            "n" : "Warehouse 1",
            "p" : "D Soft",
            "status" : 0,
            "stock" : 5
        }
    ]
}

但是我想在数组中为没有父仓库的数据提供数据,因为该人没有订阅父仓库,并且应该无法接收该数据,如下所示:

{
    "_id" : null,
    "artNr" : "ART01",
    "GTIN" : null,
    "n" : null,
    "p" : null,
    "source" : null,
    "cIds" : null,
    "warehouses" : [ 
        {
            "_id" : ObjectId("5922e263c4352e2b3ccc7b64"),
            "n" : "Warehouse 1",
            "p" : "D Soft",
            "status" : 0,
            "stock" : 5
        }
    ]
}

我现在拥有的完整查询产生了上面的示例(当然最后一个除外)是这样的(与问题顶部的查询相同,带有额外的 $group):

db.getCollection('[module].[virtualwarehouses].supplies').aggregate([
    {
        $match: { 
            $or: [ 
                { artNr: "ART01" },
                { GTIN: "GTIN001" }
            ]
        }
    },
    {
       $lookup: {
           from: '[module].[virtualwarehouses].warehouses',
           localField: 'wId',
           foreignField: '_id',
           as: 'warehouse'
       }
    },
    {
        $unwind: '$warehouse'
    },
    {
        $lookup: {
           from: '[module].[virtualwarehouses].warehouses',
           localField: 'warehouse._id',
           foreignField: 'cIds',
           as: 'warehouseP'
       }
    },
    {
       $unwind: {
            path: '$warehouseP',
            preserveNullAndEmptyArrays: true
        }
    },
    {
        $match: {
            $and: [
                {'warehouse.isDel' : false},
                {$or: [
                    { 'warehouseP.isDel' : false },
                    { 'warehouseP' : { $exists: false } }
                ]},
                {$or: [
                    { 'warehouse.subs' : { $elemMatch: { sKey: "D Soft" } } }, 
                    { 'warehouseP.subs' : { $elemMatch: { sKey: "D Soft" } } }        
                ]}
            ]
        }
    },
    {
        $group: {
            _id: '$warehouseP._id',
            artNr: { $first: '$artNr' },
            GTIN: { $first : '$GTIN' },
            n: { $first: '$warehouseP.name' },
            p: { $first: '$warehouseP.pId' },
            source: { $first: '$warehouseP.source' },
            cIds: { $first: '$warehouseP.cIds' },
            warehouses: { 
                $addToSet: { 
                    _id: '$warehouse._id',
                    n : '$warehouse.name', 
                    p : '$warehouse.pId',
                    status: '$status',
                    stock: '$stock',
                    etaStock: '$etaStock'
                } 
            }
        }
    }
])

也许这个查询的整个方法是错误的,我可以更轻松地完成吗?我对 mongoDB 没有太多经验。提前致谢。

【问题讨论】:

  • 如果不提供“小”样本数据集并至少显示您对预期结果的近似值,实际上很难说在这里做什么是“正确”的事情。这将大大有助于真正将问题摆在其他人面前,让他们可以推测出解决方案。因此,它有助于让其他人清楚地了解自己。没有数据,我们不能真正说您正在做的任何过程是最好的事情。
  • 另请注意您自己的声明“否则将超出文档限制”,这也适用于$lookup,因为它是 BSON 规范的基本限制。如果您不可能引用文档中的所有数据并因此将其放入其他集合中,那么$lookup 所做的就是“尝试将所有这些数据填充回文档中”。因此,大概会有几种措施来实际“过滤”返回的结果,以免在任何时候打破这个限制。这是一个问题需要清晰的例子。
  • @NeilLunn 感谢您对这个问题感兴趣。我一直计划在 codereview 上问一个问题,问我所做的是否是最好的方法。我计划在那里提供对我的数据模型的更详细描述。但是,由于其中的要求是代码功能齐全,并且仍然存在我的查询没有给出我想要的结果的情况,我不想冒险在那里发布并被否决。感谢您的理解。
  • @NeilLunn 对于$lookup:一个仓库可以容纳相当大量的物资。但在此查询中,我将耗材输出限制为特定商品编号/GTIN 的输出。所以,在我看来,我的输出会低得多(如果同时传入现有的 artNr 和 GTIN,每个仓库只有 2 个可能的供应品)我认为$lookup 不会导致超出文件限制。还是我弄错了?
  • 如果您添加尽可能多的解释并按照我已经在您的问题中提到的内容,那将是最好的。然后,完成后,您可以发表评论以通知已添加信息。就像我说的那样,有样本很好,以便可视化它是如何组合在一起的。

标签: mongodb aggregation-framework


【解决方案1】:

使用 Mongo 3.2 版

使用$filter

您可以使用$filter 匹配数组上的字段,然后使用$size + $gt 将布尔值投影到聚合管道中的$cond 运算符

$project: {
    warehouseP: {
        $cond: {
            if: {
                $gt: [{
                    $size: {
                        $filter: {
                            input: "$warehouseP.subs",
                            as: "result",
                            cond: {
                                $eq: ['$$result.sKey', "localhost"]
                            }
                        }
                    }
                }, 0]
            },
            then: "$warehouseP",
            else: null
        }
    }
}

使用$setIsSubset

{
    $project: {
        warehouseP: {
            $cond: {
                if: {
                    $setIsSubset: [
                        ["localhost"], "$warehouseP.subs.sKey"
                    ]
                },
                then: "$warehouseP",
                else: null
            }
        }
    }
}

使用 Mongo 3.4 版

您可以使用$in

{
    $addFields: {
        warehouseP: {
            $cond: {
                if: {
                    $in: ["localhost", "$warehouseP.subs.sKey"]
                },
                then: "$warehouseP",
                else: null
            }
        }
    }
}

参考:

https://docs.mongodb.com/manual/reference/operator/aggregation/in/ https://docs.mongodb.com/manual/reference/operator/aggregation/filter/ https://docs.mongodb.com/manual/reference/operator/aggregation/setIsSubset/

【讨论】:

  • 感谢您的回答。这些都有效,除非字段warehouseP 不存在,或者warehouseP.subs 不存在。然后我得到 errmsg:$in requires an array as a second argumend, found: missing。我一直在尝试改变 $cond,因为我没有太多成功。你有什么想法吗?
  • 您可以编写嵌套的$cond 来检查所有值,或者您可以使用$switch 以获得更好的可读性。像{ $addFields: { warehouseP: { $switch: { branches: [ { case: {$eq:[{ $ifNull: [ "$warehouseP", null ] }, null]}, then:null }, { case: {$eq:[{ $ifNull: [ "$warehouseP.subs", null ] }, null]}, then:null }, { case: {$in: ["localhost", "$warehouseP.subs.sKey"]}, then:"$warehouseP" } ], default: null } } } } 这样的东西。我会让你使用它。看看你能不能把它清理一下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-01
相关资源
最近更新 更多