MongoDB:根据字段数组中是否存在特定值,查找并修改结果

3

在MongoDB中,是否有一种简单的解决方案可以查找与查询匹配的某些对象,然后根据数组中是否包含某个值来修改结果,而不修改持久数据?

让我举个例子来说明:

students = [
  { 
    name: "Alice", 
    age: 25, 
    courses: [ { name: "Databases", credits: 6 },{ name: "Java", credits: 4 }] 
  }, 
  { 
    name: "Bob",  
    age: 22, 
    courses: [ { name: "Java", credits: 4 } ] 
  }, 
  { 
    name: "Carol", 
    age: 19, 
    courses: [ { name: "Databases", credits: 6 } ] 
  }, 
  { 
    name: "Dave", age: 18
  }
]

现在,我想查询所有学生的数据。结果应该返回除“课程”之外的所有数据。我希望输出一个标志“participant”,指示该人是否参加了数据库课程:

result = [
  { name: "Alice", age: 25, participant: 1 }, 
  { name: "Bob", age: 22, participant: 0 },
  { name: "Carol", age: 19, participant: 1 }, 
  { name: "Dave", age: 18, participant: 0}
]

不需要改变数据库中的内容。

我已经找到了使用聚合函数的解决方案。但这很复杂和不便,因此我想知道是否有更加便捷的解决方案。

我的当前解决方案如下:

db.students.aggregate([
  {$project: {"courses": {$ifNull: ["$courses", [{name: 0}]]}, name: 1, _id: 1, age: 1}}, 
  {$unwind: "$courses"}, 
  {$project: {name: 1, age: 1, participant: {$cond: [{$eq: ["$courses.name", "DB"]}, 1, 0]}}}, 
  {$group: {_id: {_id: "$_id", age: 1, name: "$name"}, participant: {$sum: "$participant"}}}, 
  {$project: {_id: 0, _id: "$_id._id", age: "$_id.age", name: "$_id.name", participant: 1}}
]);

我不喜欢这个解决方案的一个点是,我必须精确地指定输出字段三次。此外,这个管道非常长。


结果集有多大? - Marcin Bilski
我想这可能会很小。我计划稍后实现分页查询(就像在Facebook上滚动时一样),因此每次将检索到几十个对象。但是这将经常执行。 - Green绿色
2个回答

3
运行以下聚合管道以获得所需结果:
db.students.aggregate([
    {
        "$project": {
            "name": 1,
            "age": 1,
            "participant": {
                "$size": {
                    "$ifNull" : [ 
                        {
                            "$setIntersection" : [
                                {
                                    "$map": {
                                        "input": "$courses",
                                        "as": "el",
                                        "in": {
                                            "$eq": [ "$$el.name", "Databases" ]
                                        }
                                    }
                                },
                                [true]
                            ]
                        },
                        []
                    ]
                }                
            }
        }
    }
])

输出:

{
    "result" : [ 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd216"),
            "name" : "Alice",
            "age" : 25,
            "participant" : 1
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd217"),
            "name" : "Bob",
            "age" : 22,
            "participant" : 0
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd218"),
            "name" : "Carol",
            "age" : 19,
            "participant" : 1
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd219"),
            "name" : "Dave",
            "age" : 18,
            "participant" : 0
        }
    ],
    "ok" : 1
}

上述管道仅使用一个步骤,即$project,其中通过一系列嵌套运算符创建了新字段participant
关键操作是深度嵌套的$map运算符,其本质上创建一个新的数组字段,该字段保存作为子表达式中逻辑评估结果的值,并将其应用于数组的每个元素。让我们仅通过执行带有$map部分的管道来演示此操作:
db.students.aggregate([
    {
        "$project": {
            "name": 1,
            "age": 1,
            "participant": {
                "$map": {
                    "input": "$courses",
                    "as": "el",
                    "in": {
                        "$eq": [ "$$el.name", "Databases" ]
                    }
                }               
            }
        }
    }
])

输出

{
    "result" : [ 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd216"),
            "name" : "Alice",
            "age" : 25,
            "participant" : [ 
                true, 
                false
            ]
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd217"),
            "name" : "Bob",
            "age" : 22,
            "participant" : [ 
                false
            ]
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd218"),
            "name" : "Carol",
            "age" : 19,
            "participant" : [ 
                true
            ]
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd219"),
            "name" : "Dave",
            "age" : 18,
            "participant" : null
        }
    ],
    "ok" : 1
}

通过引入$setIntersection运算符进一步探测数组,该运算符返回一个包含所有输入集合中出现的元素的集合。因此,在上面的示例中,您需要获取一个结果数组,其中true表示用户参加了数据库课程,否则将返回一个空或null数组。让我们看看如何添加该运算符会影响先前的结果:

db.students.aggregate([
    {
        "$project": {
            "name": 1,
            "age": 1,
            "participant": {
                "$setIntersection" : [
                    {
                        "$map": {
                            "input": "$courses",
                            "as": "el",
                            "in": {
                                "$eq": [ "$$el.name", "Databases" ]
                            }
                        }
                    },
                    [true]
                ]                
            }
        }
    }
])

输出:

{
    "result" : [ 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd216"),
            "name" : "Alice",
            "age" : 25,
            "participant" : [ 
                true
            ]
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd217"),
            "name" : "Bob",
            "age" : 22,
            "participant" : []
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd218"),
            "name" : "Carol",
            "age" : 19,
            "participant" : [ 
                true
            ]
        }, 
        {
            "_id" : ObjectId("564f1bb67d3c273d063cd219"),
            "name" : "Dave",
            "age" : 18,
            "participant" : null
        }
    ],
    "ok" : 1
}

为了处理空值,应用$ifNull运算符,相当于SQL中的coalesce命令,用于将null值替换为空数组:
db.students.aggregate([
    {
        "$project": {
            "name": 1,
            "age": 1,
            "participant": {
                "$ifNull" : [ 
                    {
                        "$setIntersection" : [
                            {
                                "$map": {
                                    "input": "$courses",
                                    "as": "el",
                                    "in": {
                                        "$eq": [ "$$el.name", "Databases" ]
                                    }
                                }
                            },
                            [true]
                        ]
                    },
                    []
                ]                
            }
        }
    }
])

接下来,您可以使用$size操作符将$ifNull操作符包装在一起,以返回参与者数组中的元素数量,并产生如上所示的最终输出。


1
谢谢!这看起来比我的解决方案更方便! - Green绿色
@Green 没问题,很高兴能帮助 :) - chridam

0

根据您关于对象数量较少的说法,我们可以考虑直接提取数据库名称并使用JavaScript map进行转换。这样做不仅在传输方面节省了很多,而且代码比管道更易读。


你的意思是,只需调用db.students.find()而不需要“提取数据库名称”吗? - Green绿色

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接