如何从条件中删除输出中的不需要的字段

时间:2017-09-04 13:29:23

标签: javascript node.js mongodb aggregation-framework

我有一个投影阶段如下,

{
  'name': {$ifNull: [ '$invName', {} ]},,
  'info.type': {$ifNull: [ '$invType', {} ]},
  'info.qty': {$ifNull: [ '$invQty', {} ]},
  'info.detailed.desc': {$ifNull: [ '$invDesc', {} ]}
}

如果字段不存在,我正在投影空对象({}),因为如果在字段中执行排序并且字段不存在,那么该文档将按排序顺序排在第一位({{ 3}})。下一阶段是排序,并希望不存在的字段在排序顺序中排在最后。这是按预期工作的。

现在,我想删除那些具有空对象作为值的字段(如果info.detailed.desc为空info.detailed不应该在输出中)。我可以使用lodash像这样(Sort Documents Without Existing Field to End of Results)在节点级别执行此操作。但我试图在mongodb级别这样做。可能吗?我试过$redact,但是它会过滤掉整个文档。是否可以基于值来处理文档的PRUNEDESCEND字段?

2 个答案:

答案 0 :(得分:1)

从文档中完全删除属性并非易事。基础是服务器本身在MongoDB 3.4和$replaceRoot之前没有任何方法可以做到这一点,它基本上允许表达式作为文档上下文返回。

即使有这样的添加,如果没有MongoDB 3.4.4中引入的$objectToArray$arrayToObject的其他功能,这样做也有些不切实际。但要经历这些案件。

使用快速示例

{ "_id" : ObjectId("59adff0aad465e105d91374c"),  "a" : 1 }
{ "_id" : ObjectId("59adff0aad465e105d91374d"),  "a" : {} }

有条件地返回根对象

db.junk.aggregate([
  { "$replaceRoot": {
    "newRoot": {
      "$cond": {
        "if": { "$ne": [ "$a", {} ] },
        "then": "$$ROOT",
        "else": { "_id": "$_id" }
      }
    }    
  }}
])

这是一个非常简单的原则,实际上可以应用于任何嵌套属性以删除它的子键,但需要不同级别的嵌套$cond甚至$switch申请可能的条件。当然,$replaceRoot需要"顶级"删除,因为它是有条件地表达顶级键返回的唯一方法。

因此,虽然理论上你可以使用$cond$switch来决定返回什么,但它通常很麻烦,你会想要更灵活的东西。

过滤空对象

db.junk.aggregate([
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$filter": {
          "input": { "$objectToArray": "$$ROOT" },
          "cond": { "$ne": [ "$$this.v", {} ] }
        }
      }
    }
  }}
])

这是$objectToArray$arrayToObject开始使用的地方。我们只是将对象内容转换为"数组"而不是为每个可能的键写出条件。并在数组条目上应用$filter来决定保留什么。

$objectToArray将任何对象转换为表示每个属性的文档数组,其中"k"表示密钥的名称,"v"表示该属性的值。由于这些现在可以作为"值"来访问,因此您可以使用$filter之类的方法来检查每个数组条目并丢弃不需要的数组条目。

最后$arrayToObject采取"过滤"内容并将这些"k""v"值转换回属性名称和值作为结果对象。通过这种方式,"过滤器"条件从结果对象中删除任何不符合条件的属性。

返回$ cond

db.junk.aggregate([
  { "$project": {
    "a": { "$cond": [{ "$eq": [ "$a", {} ] }, "$$REMOVE", "$a" ] }    
  }}
])

MongoDB 3.6引入了一个$$REMOVE常量的新播放器。这是一项新功能,可以与$cond一起应用,以决定是否显示该属性。这是另一种方法,当然可以发布。

在上述所有情况下,当值为我们要测试删除的空对象时,不会返回"a"属性。

{ "_id" : ObjectId("59adff0aad465e105d91374c"),  "a" : 1 }
{ "_id" : ObjectId("59adff0aad465e105d91374d") }

更复杂的结构

此处您的具体要求是包含嵌套属性的数据。因此,我们可以继续使用概述的方法来演示如何完成。

首先是一些样本数据:

{ "_id" : ObjectId("59ae03bdad465e105d913750"), "a" : 1, "info" : { "type" : 1, "qty" : 2, "detailed" : { "desc" : "this thing" } } }
{ "_id" : ObjectId("59ae03bdad465e105d913751"), "a" : 2, "info" : { "type" : 2, "qty" : 3, "detailed" : { "desc" : {  } } } }
{ "_id" : ObjectId("59ae03bdad465e105d913752"), "a" : 3, "info" : { "type" : 3, "qty" : {  }, "detailed" : { "desc" : {  } } } }
{ "_id" : ObjectId("59ae03bdad465e105d913753"), "a" : 4, "info" : { "type" : {  }, "qty" : {  }, "detailed" : { "desc" : {  } } } }

应用过滤方法

db.junk.aggregate([
  { "$replaceRoot": {
    "newRoot": {
      "$arrayToObject": {
        "$filter": {
          "input": {
            "$concatArrays": [
              { "$filter": {
                "input": { "$objectToArray": "$$ROOT" },
                "cond": { "$ne": [ "$$this.k", "info" ] }    
              }},
              [
                { 
                  "k": "info", 
                  "v": {
                    "$arrayToObject": {
                      "$filter": {
                        "input": { "$objectToArray": "$info" },
                        "cond": {
                          "$not": {
                            "$or": [
                              { "$eq": [ "$$this.v", {} ] },
                              { "$eq": [ "$$this.v.desc", {} ] }
                            ]      
                          }
                        }
                      }
                    }
                  }
                }
              ]
            ]
          },
          "cond": { "$ne": [ "$$this.v", {} ] }
        }
      }
    }
  }}
])

由于嵌套级别,这需要更复杂的处理。在这里的主要情况下,您需要独立查看"info"键,并删除任何不符合条件的子属性。由于您需要返回"某些东西",我们基本上需要在删除所有内部属性时删除"info"密钥。这就是对每组结果进行嵌套过滤操作的原因。

将$ cond与$$ REMOVE一起使用

如果可以的话,这首先看起来更合乎逻辑,所以首先从最简化的形式看这个是有帮助的:

db.junk.aggregate([
  { "$addFields": {
    "info.type": { 
      "$cond": [
        { "$eq": [ "$info.type", {} ] },
        "$$REMOVE",
        "$info.type"
      ]
    },
    "info.qty": {
      "$cond": [
        { "$eq": [ "$info.qty", {} ] },
        "$$REMOVE",
        "$info.qty"
      ]
    },
    "info.detailed.desc": {
      "$cond": [
        { "$eq": [ "$info.detailed.desc", {} ] },
        "$$REMOVE",
        "$info.detailed.desc"
      ]
    }
  }}
])

但是你需要查看它实际产生的输出:

/* 1 */
{
    "_id" : ObjectId("59ae03bdad465e105d913750"),
    "a" : 1.0,
    "info" : {
        "type" : 1.0,
        "qty" : 2.0,
        "detailed" : {
            "desc" : "this thing"
        }
    }
}

/* 2 */
{
    "_id" : ObjectId("59ae03bdad465e105d913751"),
    "a" : 2.0,
    "info" : {
        "type" : 2.0,
        "qty" : 3.0,
        "detailed" : {}
    }
}

/* 3 */
{
    "_id" : ObjectId("59ae03bdad465e105d913752"),
    "a" : 3.0,
    "info" : {
        "type" : 3.0,
        "detailed" : {}
    }
}

/* 4 */
{
    "_id" : ObjectId("59ae03bdad465e105d913753"),
    "a" : 4.0,
    "info" : {
        "detailed" : {}
    }
}

虽然删除了其他键,但"info.detailed"仍然存在,因为在此级别上没有任何实际测试。事实上,你根本无法用简单的术语表达,所以解决这个问题的唯一方法是将对象作为表达式进行评估,然后在每个输出级别上应用额外的过滤条件,以查看空对象仍然驻留的位置,并删除它们:

db.junk.aggregate([
  { "$addFields": {
    "info": {
      "$let": {
        "vars": {
          "info": {
            "$arrayToObject": {  
              "$filter": {
                "input": {
                  "$objectToArray": {
                    "type": { "$cond": [ { "$eq": [ "$info.type", {} ] },"$$REMOVE", "$info.type" ] },
                    "qty": { "$cond": [ { "$eq": [ "$info.qty", {} ] },"$$REMOVE", "$info.qty" ] },
                    "detailed": {
                      "desc": { "$cond": [ { "$eq": [ "$info.detailed.desc", {} ] },"$$REMOVE", "$info.detailed.desc" ] }
                    }
                  }
                },
                "cond": { "$ne": [ "$$this.v", {} ] }
              }
            }
          }    
        },
        "in": { "$cond": [ { "$eq": [ "$$info", {} ] }, "$$REMOVE", "$$info" ] }
      }    
    }
  }}
])

与普通$filter方法一样,这种方法实际上删除了所有"结果中的空对象:

/* 1 */
{
    "_id" : ObjectId("59ae03bdad465e105d913750"),
    "a" : 1.0,
    "info" : {
        "type" : 1.0,
        "qty" : 2.0,
        "detailed" : {
            "desc" : "this thing"
        }
    }
}

/* 2 */
{
    "_id" : ObjectId("59ae03bdad465e105d913751"),
    "a" : 2.0,
    "info" : {
        "type" : 2.0,
        "qty" : 3.0
    }
}

/* 3 */
{
    "_id" : ObjectId("59ae03bdad465e105d913752"),
    "a" : 3.0,
    "info" : {
        "type" : 3.0
    }
}

/* 4 */
{
    "_id" : ObjectId("59ae03bdad465e105d913753"),
    "a" : 4.0
}

在代码

中完成所有操作

所以这里的一切都取决于最新的功能或者确实"即将推出的功能"在您使用的MongoDB版本中可用。如果这些不可用,则替代方法是简单地从游标返回的结果中删除空对象。

它通常是最理智的事情,除非聚合管道需要继续超过字段被删除的点,否则确实是您所需要的。即便如此,您可能应该在逻辑上解决这个问题,并将最终结果留给光标处理。

作为shell的JavaScript,您可以使用以下方法,无论实际语言实现如何,原则基本保持不变:

db.junk.find().map( d => {
  let info = Object.keys(d.info)
    .map( k => ({ k, v: d.info[k] }))
    .filter(e => !(
      typeof e.v === 'object' && 
     ( Object.keys(e.v).length === 0 || Object.keys(e.v.desc).length === 0 ) 
    ))
    .reduce((acc,curr) => Object.assign(acc,{ [curr.k]: curr.v }),{});
  delete d.info;
  return Object.assign(d,(Object.keys(info).length !== 0) ? { info } : {})
})

这几乎是与上述示例相同的本地语言方式,即其中一个预期属性包含空对象,从输出中完全删除该属性。

答案 1 :(得分:0)

我在聚合管道的末尾使用 $project 删除了输出 JSON 中的品牌对象

db.Product.aggregate([
        {
          $lookup: {
            from: "wishlists",
            let: { product: "$_id" },
            pipeline: [
              {
                $match: {
                  $and: [
                    { $expr: { $eq: ["$$product", "$product"] } },
                    { user: userId }
                  ]
                }
              }
            ],
            as: "isLiked"
          }
        },
        {
          $lookup: {
            from: "brands",
            localField: "brand",
            foreignField: "_id",
            as: "brands"
          }
        },
        {
          $addFields: {
            isLiked: { $arrayElemAt: ["$isLiked.isLiked", 0] }
          }
        },
        {
          $unwind: "$brands"
        },
        {
           $addFields: {
                    "brand.name": "$brands.name" ,
                    "brand._id": "$brands._id"
                 }
            },
        {
           $match:{ isActive: true }
        },
        { 
           $project: { "brands" : 0 } 
        }
      ]);
相关问题