首页 专题 H5案例 前端导航 UI框架

Mongoose开发实战-高级篇

作者:TG 日期: 2017-11-21 阅读: 1523
今天的主题是Mongoose中的聚合函数(Aggregate)。
在学习聚合函数之前,我们需要了解两个概念:管道表达式

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

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

管道是可重复的。

表达式

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

下面开始学习聚合函数。

语法:

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":"张三"}]}]


相关文章:


如有任何问题或疑问,欢迎在下方评论区留言!


关注”全栈技术杂货铺“

全栈技术杂货铺