Mongoose 中的 aggregate 用法--随笔
Mongoose
中的 aggregate()
是一种强大的工具,用于对 MongoDB 数据库中的集合进行复杂的数据查询和处理操作。它利用 MongoDB 的 aggregation pipeline 来执行数据聚合操作,比如过滤、分组、排序、投影、连接(lookup
)等。下面将详细介绍 aggregate()
的使用方法。
1. 基本语法
Model.aggregate(pipeline, options)
pipeline
:数组,包含了一系列的数据处理阶段(stages),每个阶段都表示一种数据处理操作。options
(可选):一些额外的选项,用于控制聚合操作的行为,比如allowDiskUse
(允许使用磁盘进行操作)。
2. 常用的 Aggregation Pipeline 阶段
$match:过滤数据,相当于
find()
中的条件过滤。$group:分组数据,常用于统计或聚合。
$project:修改文档的结构,选择性输出字段。
$sort:排序数据。
$limit:限制返回的文档数。
$lookup:关联(连接)另一个集合的数据,类似于 SQL 的
JOIN
。$unwind:展开数组字段中的每个元素为独立的文档。
3. 基本示例
假设有一个 User
模型,我们来演示一些常见的 aggregate()
用法:
示例 1:$match
和 $group
统计用户的年龄分布
User.aggregate([
{
$match: { isActive: true } // 只选择活跃的用户
},
{
$group: {
_id: "$age", // 根据年龄分组
total: { $sum: 1 } // 统计每个年龄段的人数
}
}
])
.then(result => console.log(result))
.catch(err => console.error(err));
这个管道会先过滤出 isActive
为 true
的用户,然后根据 age
进行分组,并统计每个年龄段的用户数。
示例 2:$project
和 $sort
选取特定字段并排序
User.aggregate([
{
$project: { // 只返回 name 和 age 字段
name: 1,
age: 1,
_id: 0 // 不返回 _id 字段
}
},
{
$sort: { age: -1 } // 按年龄从大到小排序
}
])
.then(result => console.log(result))
.catch(err => console.error(err));
这个管道将只返回 name
和 age
字段,并按年龄进行降序排列。
示例 3:$lookup
实现关联查询(JOIN 操作)
假设有两个集合 orders
和 customers
,你希望将每个订单与对应的客户数据关联起来:
Order.aggregate([
{
$lookup: {
from: "customers", // 关联的集合名称
localField: "customerId", // 当前集合中的字段
foreignField: "_id", // 关联集合中的字段
as: "customerDetails" // 输出的关联数据放入 customerDetails 数组中
}
},
{
$unwind: "$customerDetails" // 将 customerDetails 数组展开成文档
}
])
.then(result => console.log(result))
.catch(err => console.error(err));
这个聚合管道将 orders
集合中的 customerId
与 customers
集合中的 _id
进行匹配,返回每个订单对应的客户详情。
示例 4:$unwind
处理数组字段
如果文档中的某个字段是一个数组,比如 User
文档中有一个 hobbies
数组,想要对每个兴趣进行统计,可以用 $unwind
:
User.aggregate([
{
$unwind: "$hobbies" // 将每个兴趣项展开
},
{
$group: {
_id: "$hobbies", // 按兴趣分组
total: { $sum: 1 } // 统计每个兴趣的人数
}
},
{
$sort: { total: -1 } // 按兴趣人数排序
}
])
.then(result => console.log(result))
.catch(err => console.error(err));
这个例子会统计所有用户的兴趣爱好,并按兴趣人数降序排列。
4. 使用 Aggregation 中的管道变量
MongoDB 的聚合框架允许你在管道中使用变量,比如 $
前缀的变量可以引用文档字段。例如,想要统计每个用户年龄是否超过 30 岁:
User.aggregate([
{
$project: {
name: 1,
isOver30: { $gt: ["$age", 30] } // 计算年龄是否超过30岁
}
}
])
.then(result => console.log(result))
.catch(err => console.error(err));
5. 聚合选项
可以通过在 aggregate()
方法中传入选项来控制聚合行为。例如,当数据量较大时,启用 allowDiskUse
选项允许 MongoDB 使用磁盘进行处理:
User.aggregate(pipeline, { allowDiskUse: true })
.then(result => console.log(result))
.catch(err => console.error(err));
问题记录
- 使用 $lookup localField 为字符串,foreignField 为 ObjectId,匹配不上,需要做一定的转换
解决方案 1:将 **article_id**
转换为 **ObjectId**
可以使用 $addFields
将字符串的 article_id
转换为 ObjectId
,然后再进行 $lookup
匹配。
js复制代码db.comments.aggregate([
{
$addFields: {
articleObjectId: { $toObjectId: "$article_id" } // 将字符串 article_id 转换为 ObjectId
}
},
{
$lookup: {
from: 'articles',
localField: 'articleObjectId', // 使用转换后的 ObjectId 进行匹配
foreignField: '_id',
as: 'articleDetails'
}
}
])
这个管道首先通过 $addFields
创建了一个新的字段 articleObjectId
,它将 article_id
字符串转换成 ObjectId
,然后在 $lookup
阶段使用这个新的字段进行匹配。
解决方案 2:将 **articles**
集合中的 **_id**
转换为字符串
如果你无法修改 comments
集合的 article_id
,可以在 $lookup
阶段将 articles
集合中的 _id
转换为字符串来进行匹配:
js复制代码db.comments.aggregate([
{
$lookup: {
from: 'articles',
let: { articleIdStr: "$article_id" }, // 把 comment 中的 article_id 传递到 $lookup
pipeline: [
{
$addFields: { _idStr: { $toString: "$_id" } } // 将 articles 中的 _id 转换为字符串
},
{
$match: { $expr: { $eq: ["$_idStr", "$$articleIdStr"] } } // 匹配字符串
}
],
as: 'articleDetails'
}
}
])
总结
mongoose.aggregate()
是一种强大的工具,允许你执行复杂的数据操作。通过管道(pipeline)将多个操作组合在一起,你可以轻松进行过滤、排序、分组、关联等操作。结合 MongoDB 的 Aggregation Pipeline,它可以处理大量复杂的查询和聚合需求。