Mongoose开发实战-高级篇

今天的主题是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

更多管道操作符:docs.mongodb.com/manual/refe…
对于$group的表达式:

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

更多表达式:docs.mongodb.com/manual/meta…
是不是看得眼花缭乱,还是用实例来学习吧!

完整实例:
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":"张三"}]}]复制代码

相关文章:

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