从文档中完全删除属性并非易事。基本情况是,在 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 } : {})
})
这几乎是本地语言的表述方式,与上面的示例相同,即其中一个预期属性包含一个空对象,从输出中完全删除该属性。