我有一个投影阶段如下,
{
'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
,但是它会过滤掉整个文档。是否可以基于值来处理文档的PRUNE
或DESCEND
字段?
答案 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"
值转换回属性名称和值作为结果对象。通过这种方式,"过滤器"条件从结果对象中删除任何不符合条件的属性。
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 }
}
]);