原文连接

    今天的主题是Mongoose中的聚合函数(Aggregate)。
    在学习聚合函数之前,我们需要了解两个概念:管道表达式

    如果你了解管道的概念,你可以跳过这一段话。如果你不了解,我在这里打个比方,比如生活中的水管,水(也就是我们的数据源)源源不断的从一节(一个管道)流向另一节(另一个管道);如果对某一节(一个管道)的水做了一些处理(也就是数据的筛选,排序等),那么在下一节(另一个管道)接收到的水就是你处理后的,当然,你也可以再次处理,如此反复…最后流到你家的就是经过层层处理的水(也就是我们需要得到的数据)。

    而Mongoose的聚合函数的原理就是这样,后一个管道得到的数据是上一个管道处理后的数据…。

    管道是可重复的。

    表达式

    表达式很简单,你可以理解为计算。在Mongoose中主要处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。

    下面开始学习聚合函数。

    语法:

    1. db.COLLECTION_NAME.aggregate(OPERATION CALLBACK)

    OPERATION: Object | Array,可选
    CALLBACK: 可选

    管道

    • $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。对应project()方法
    • $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。对应match()
    • $limit:用来限制MongoDB聚合管道返回的文档数。对应limit()方法
    • $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。对应skip()
    • $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。对应unwind()方法
    • $group:将集合中的文档分组,可用于统计结果。对应group()方法
    • $sort:将输入文档排序后输出。对应sort()方法
    • $geoNear:输出接近某一地理位置的有序文档。对应near()

    v3.2

    • $sample:随机选择N个
    • $lookup:连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于populate

    更多管道操作符:https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/

    对于$group的表达式:

    • $sum 计算总和。
    • $avg 计算平均值
    • $min 获取集合中所有文档对应值得最小值。
    • $max 获取集合中所有文档对应值得最大值。
    • $push 在结果文档中插入值到一个数组中。
    • $addToSet 在结果文档中插入值到一个数组中,但不创建副本。
    • $first 根据资源文档的排序获取第一个文档数据。
    • $last 根据资源文档的排序获取最后一个文档数据

    更多表达式:https://docs.mongodb.com/manual/meta/aggregation-quick-reference/#aggregation-expressions

    是不是看得眼花缭乱,还是用实例来学习吧!

    完整实例:mongodb-pratice

    首先往citys集合中插入如下数据:

    {province: 'guangdong', city: 'guangzhou', person: 600, industry: [{name: 'IT', person: 200}, {name: 'teacher', person: 400}]}  
    {province: 'guangdong', city: 'shenzhen', person: 700, industry: [{name: 'IT', person: 300}, {name: 'teacher', person: 400}]}   
    {province: 'beijing', city: 'beijing', person: 600, industry: [{name: 'IT', person: 350}, {name: 'teacher', person: 250}]}
    

    看看实例(1),查询人口超过1000的省份:

    // modules/citys/citys.controller.js   
    exports.getCityGtThousand = (req, res) => {   
      CityModel.aggregate([   
        {$group: {_id: '$province', total: {$sum: '$person'}}},   
        {$match: {total: {$gt: 1000}}}   
      ], (err, result) => {   
        ...
      })
    }
    

    在上面的代码中,$province引用的是原文档中的province,如果在前面的管道中有同名属性,则后续引用的是最后被赋值的,详情请参考实例(2)

    注意:使用$group时,_id是必须的,用作分组的依据条件。

    结果如下:

    [{"_id":"guangdong","total":1300}]
    

    上面的聚合查询相当于SQL:

    SELECT province, SUM(person) AS total FROM citys GROUP BY province HAVING total > 1000
    

    其实上面的查询可分为两步(两个管道):

    1. 将相同省份的城市的人口加起来,并命名为total,并且将province属性赋值给_id

    CityModel.aggregate([   
      {$group: {_id: '$province', total: {$sum: '$person'}}}   
    ])
    

    得到的结果是:

    [{"_id":"beijing","total":600},{"_id":"guangdong","total":1300}]
    

    2. 比较total,返回大于1000的数据

    CityModel.aggregate([   
      {$group: {_id: '$province', total: {$sum: '$person'}}},   
      {$match: {total: {$gt: 1000}}}   
    ])
    

    最终结果就是:

    [{"_id":"guangdong","total":1300}]
    

    下面我们来看看实例(2),查询总IT人口大于400的省份且返回所有城市名称。

    看着是不是有点复杂,其实当你像实例(1)一样拆分开来,你会发现很简单:
    1. 计算每个省份的IT总人口
    2. IT总人口大于400
    3. 返回所有城市名称

    看看代码:

    // modules/citys/citys.controller.js
    exports.getITPerson = (req, res) => {   
      CityModel.aggregate([   
        {$unwind: '$industry'},   
        {$match: {'industry.name': 'IT'}},   
        {   
          $group: {   
            _id: {province: '$province'}, itTotal: {$sum: '$industry.person'}, city: {$push: '$city'}   
          }   
        },   
        {$match: {itTotal: {$gt: 400}}}   
      ]).exec((err, result) => {
        ...
      })  
    }
    

    我们依旧来分步看看上面的查询。

    (1) $unwind用来拆分数组:

    CityModel.aggregate([   
      {$unwind: '$industry'}   
    ])
    

    查询结果:

    [   
      {   
        "_id": "5a0e83e3aea7e7fba7ff0ab2",   
        "province": "guangdong",   
        "city": "guangzhou",   
        "person": 600,   
        "industry": {   
          "name": "IT",   
          "person": 200   
        }   
      },   
      {   
        "_id": "5a0e83e3aea7e7fba7ff0ab2",   
        "province": "guangdong",   
        "city": "guangzhou",   
        "person": 600,   
        "industry": {   
          "name": "teacher",   
          "person": 400   
        }   
      }  
      ...  
    ]
    

    industry数组中每一项拆分开来,其他字段一一复制。

    (2) $match很简单,筛选匹配,类似find():

    {$match: {'industry.name': 'IT'}}
    

    筛选行业为IT的数据

    (3) 接下来执行$group

    $group: {   
      _id: {province: '$province'}, itTotal: {$sum: '$industry.person'}, city: {$push: '$city'}   
    }
    

    根据省份province分组,将同一个省份的IT人口相加,同时将城市名称放到一个名为city的数组中。

    (4) 最后再次筛选:

    {$match: {itTotal: {$gt: 400}}}
    

    返回总IT人口大于400的数据。

    其他管道操作符详解

    (1) 排序

    语法:

    { $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }
    

    1是正序,-1是反序

    实例:以城市总人口排序,且值返回城市/省份/人数三个字段:

    // modules/citys/citys.controller.js
    CityModel.aggregate([
            { $sort: {person: 1} },
            {
                $project: {
                    _id: 0,
                    province: 1,
                    person: 1,
                    city: 1
                }
            }
        ])
    

    注意:在Mongoose 3.4前,只有_id(默认显示)才可以指定为0或false,其他字段默认不进入下一个管道,可以设置为1或true进入下一个管道。(3.4版本可以将其他字段设为0或false)

    (2) 限制和跳过

    语法:

    { $limit: <positive integer> }
    { $skip: <positive integer> }
    

    实例:从第二条数据开始,返回一条数据:

    // modules/citys/citys.controller.js
    CityModel.aggregate([
            {$skip: 1},
            {$limit: 1}
        ])
    

    (3) 随机

    语法:

    { $sample: { size: <positive integer> } }
    

    实例:随机返回一条数据:

    // modules/citys/citys.controller.js
    CityModel.aggregate({$sample: {size: 1}})
    

    注:Mongoose 3.2后才有

    (4) 联表

    在《Mongoose 开发实战:进阶篇》中,我们讲解了联表查询populate。在Mongoose 3.2后,我们可以更方便的连接表。

    语法:

    {
       $lookup:
         {
           from: <collection to join>,
           localField: <field from the input documents>,
           foreignField: <field from the documents of the "from" collection>,
           as: <output array field>
         }
    }
    

    参数说明:

    • from 需要关联的集合名
    • localField 本集合中需要查找的字段
    • foreignField 另外一个集合中需要关联的字段
    • as 输出的字段名

    实例

    我们往users集合里面插入三条数据:

    {name: '张三', age: '22', sex: 'male' , phone: '13123123123', address: {
      city: 'guangzhou'
    }},
    {name: '李四', age: '19', sex: 'male' , phone: '13123123123', address: {
      city: 'beijing'
    }},
    {name: '王五', age: '25', sex: 'male' , phone: '13123123123', address: {
      city: 'guangzhou'
    }}
    

    对应name名称,再往文章中插入三条数据:

    {title: 'Mongoose 开发实战:基础篇', content: '讲解连接数据库,建文档等', author: '张三'},
      {title: 'Mongoose 开发实战:进阶篇', content: '讲解建索引,添加验证器等', author: '李四'},
      {title: 'Mongoose 开发实战:高级篇', content: '讲解聚合函数', author: '王五'}
    

    现在要查询张三发表的文章:

    // modules/users/users.controller.js
    ...
    UsersModel.aggregate([
            {
                $lookup: {
                    from: 'articles',
                    localField: 'name',
                    foreignField: 'author',
                    as: 'userArticle'
                }
            }, {
                $project: {
                    _id: 0,
                    name: 1,
                    'userArticle.title': 1,
                    'userArticle.author': 1
                }
            },
            {
                $match: {name: '张三'}
            }
        ])
    ...
    // [{"name":"张三","userArticle":[{"title":"Mongoose 开发实战:基础篇","author":"张三"}]}]
    

    相关文章:

    著作权归作者所有。
    商业转载请联系作者获得授权,非商业转载请注明出处。
    原文: https://laixiazheteng.com/article/1NSGRZGU93cy © ghmagical.com著作权归作者所有。
    商业转载请联系作者获得授权,非商业转载请注明出处。
    原文: https://laixiazheteng.com/article/1NSGRZGU93cy © ghmagical.com著作权归作者所有。
    商业转载请联系作者获得授权,非商业转载请注明出处。
    原文: https://laixiazheteng.com/article/1NSGRZGU93cy © ghmagical.com