【问题标题】:How to Remove Unwanted Fields from Output by Condition如何按条件从输出中删除不需要的字段
【发布时间】:2017-09-04 13:29:22
【问题描述】:

我有一个投影阶段如下,

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

如果字段不存在,我将投影空对象({}),因为如果在字段中执行排序并且该字段不存在,则该文档按排序顺序排在第一位(Sort Documents Without Existing Field to End of Results) .下一阶段是排序,并希望不存在的字段在排序顺序中排在最后。这按预期工作。

现在,我想删除那些将空对象作为值的字段(如果 info.detailed.desc 为空 info.detailed 不应该出现在输出中)。我可以像这样使用lodash 在节点级别执行此操作(https://stackoverflow.com/a/38278831/6048928)。但我正在尝试在 mongodb 级别执行此操作。是否可以?我试过$redact,但它正在过滤掉整个文档。是否可以根据值来PRUNEDESCEND 文档的字段?

【问题讨论】:

    标签: javascript node.js mongodb aggregation-framework


    【解决方案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" 键本身。这就是对每组结果进行嵌套过滤操作的原因。

    通过 $$REMOVE 应用 $cond

    在可行的情况下,这似乎是一个更合乎逻辑的选择,因此首先从最简化的形式来看会有所帮助:

    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 } : {})
    })
    

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

    【讨论】:

    • 感谢您提供如此详细的解释:) 现在我想知道 MongoDB 实现这样的事情的可能性。根据您的建议,我将采用最后一种方法,因为投影是修改文档的聚合管道的最后阶段。我以为 MongoDB 会有一个非常简单的解决方案,结果没有。使用$objectToArray$arrayToObject 的方式真的很棒。再次感谢:)
    【解决方案2】:

    我已在聚合管道末尾使用 $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 } 
            }
          ]);
    

    【讨论】:

      猜你喜欢
      • 2014-03-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-12
      • 1970-01-01
      相关资源
      最近更新 更多