一、技术实战
1.1 核心依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
1.2 application.yml
yaml
server:
port: 8080
spring:
data:
mongodb:
# 带密码
# uri: mongodb://admin:123456@localhost:27017/open_db?authSource=admin
# 无密码
uri: mongodb://localhost:27017/open_db
logging:
level:
org.springframework.data.mongodb.core.MongoTemplate: DEBUG
1.3 数据准备
- user 集合
javascript
db.user.insertMany([
{ _id: NumberLong("1"), name: "张三", city: "北京", vipLevel: "gold" },
{ _id: NumberLong("2"), name: "李四", city: "上海", vipLevel: "platinum" },
{ _id: NumberLong("3"), name: "王五", city: "广州", vipLevel: "gold" },
{ _id: NumberLong("4"), name: "赵六", city: "深圳", vipLevel: "silver" },
{ _id: NumberLong("5"), name: "周杰", city: "北京", vipLevel: "bronze" }
])
- order 集合
javascript
db.order.insertMany([
// 张三
{ userId: NumberLong("1"), status: "PAID", amount: NumberDecimal("199.00"), createTime: ISODate("2025-01-15T10:00:00Z") },
{ userId: NumberLong("1"), status: "SHIPPED", amount: NumberDecimal("50.00"), createTime: ISODate("2025-01-20T12:00:00Z") },
{ userId: NumberLong("1"), status: "DONE", amount: NumberDecimal("320.00"), createTime: ISODate("2025-02-05T09:30:00Z") },
{ userId: NumberLong("1"), status: "CANCELLED", amount: NumberDecimal("89.90"), createTime: ISODate("2025-03-10T14:20:00Z") },
{ userId: NumberLong("1"), status: "PAID", amount: NumberDecimal("450.00"), createTime: ISODate("2025-04-01T11:00:00Z") },
// 李四
{ userId: NumberLong("2"), status: "PAID", amount: NumberDecimal("299.00"), createTime: ISODate("2025-02-10T09:00:00Z") },
{ userId: NumberLong("2"), status: "PAID", amount: NumberDecimal("150.00"), createTime: ISODate("2025-02-18T14:00:00Z") },
{ userId: NumberLong("2"), status: "SHIPPED", amount: NumberDecimal("99.99"), createTime: ISODate("2025-03-05T16:45:00Z") },
{ userId: NumberLong("2"), status: "DONE", amount: NumberDecimal("780.00"), createTime: ISODate("2025-04-10T08:15:00Z") },
// 王五
{ userId: NumberLong("3"), status: "PAID", amount: NumberDecimal("520.00"), createTime: ISODate("2025-01-25T13:20:00Z") },
{ userId: NumberLong("3"), status: "REFUNDED", amount: NumberDecimal("200.00"), createTime: ISODate("2025-02-28T10:10:00Z") },
{ userId: NumberLong("3"), status: "PAID", amount: NumberDecimal("89.00"), createTime: ISODate("2025-03-15T11:45:00Z") },
// 赵六
{ userId: NumberLong("4"), status: "PAID", amount: NumberDecimal("45.50"), createTime: ISODate("2025-01-08T09:00:00Z") },
{ userId: NumberLong("4"), status: "SHIPPED", amount: NumberDecimal("320.00"), createTime: ISODate("2025-02-20T15:30:00Z") },
{ userId: NumberLong("4"), status: "DONE", amount: NumberDecimal("670.00"), createTime: ISODate("2025-04-05T12:00:00Z") },
// 周杰
{ userId: NumberLong("5"), status: "PAID", amount: NumberDecimal("128.00"), createTime: ISODate("2025-03-22T18:30:00Z") },
{ userId: NumberLong("5"), status: "CANCELLED", amount: NumberDecimal("55.00"), createTime: ISODate("2025-04-08T07:20:00Z") }
])
1.4. 实体类
java
@Document(collection = "order")
@Data
public class Order {
@Id
private String id;
// 用户ID
private Long userId;
// 订单状态
private String status;
// 订单金额, 默认情况下MongoDB会把BigDecimal存成double类型。
@Field(targetType = FieldType.DECIMAL128)
private BigDecimal amount;
// 创建时间 //东八区比 UTC 快 8 小时
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
1.5 核心代码实现
1.5.1 Service层
java
package cn.cjc.service;
import org.bson.Document;
import org.bson.types.Decimal128;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Service
public class OrderAggService {
@Autowired
private MongoTemplate mongoTemplate;
public List<Document> sumByMonthForPaidOrders() {
MatchOperation matchOperation = Aggregation.match(Criteria.where("status").is("PAID"));
ProjectionOperation projectionOperation = Aggregation.project("amount")
// 精简写法:适用于Date类型、时间戳数字、时间字符串, 不带toDate($)只有字段时只适用原生Date类型
.andExpression("dateToString('%Y-%m', toDate($createTime))").as("month")
// 原生MongoDB语法: 全部适用
//.andExpression("{$dateToString: {format: '%Y-%m', date: {$toDate: '$createTime'}}}").as("month")
// 原生MongoDB语法: 适用时间字符串
//.andExpression("{$dateToString: {format: '%Y-%m', date: {$dateFromString: {dateString: '$createTime'}}}}").as("month")
;
GroupOperation groupOperation = Aggregation.group("month")
.first("month").as("month")
.sum("amount").as("totalAmount");
// 方式一:最细粒度,手动创建 Sort.Order 对象,代码偏长
//Aggregation.sort(Sort.by(Sort.Order.desc("month"), Sort.Order.desc("day")));
// 方式二:流式链式调用
//Aggregation.sort(Sort.by("month").descending())
// 方式三:老式重载方法,不推荐
//Aggregation.sort(Sort.Direction.DESC, "month")
// 方式四:推荐标准写法, 分组必须输出month排序才生效
SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "month"));
ProjectionOperation rename = Aggregation.project()
.and("_id").as("month")
.and("totalAmount").as("totalAmount")
.andExclude("_id");
Aggregation aggregation = Aggregation.newAggregation(matchOperation, projectionOperation, groupOperation, sortOperation, rename);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
public List<Document> qryOrderWithUser() {
LookupOperation lookupOperation = LookupOperation.newLookup()
.from("user")
.localField("userId")
.foreignField("_id")
.as("userInfo");
UnwindOperation unwindOperation = Aggregation.unwind("userInfo");
SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.ASC, "createTime"));
ProjectionOperation projectionOperation = Aggregation.project()
.and("amount").as("orderAmount")
.and("status").as("orderStatus")
.andExpression("{ $dateToString: { format: '%Y-%m-%d %H:%M:%S', date: '$createTime', timezone: 'Asia/Shanghai' } }")
.as("createTime")
.and("userInfo.name").as("userName")
// 原始ObjectId转换为字符串
.andExpression("{ $toString: '$_id' }").as("orderId")
// 原始ObjectId转换为时间戳加日期的字符串
//.and("_id").as("orderId")
.andExclude("_id");
Aggregation aggregation = Aggregation.newAggregation(lookupOperation, sortOperation, unwindOperation, projectionOperation);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
public List<Document> filterUserByTotalAmount(BigDecimal minTotalAmount) {
GroupOperation groupOperation = Aggregation.group("userId")
.sum("amount").as("totalAmount")
.count().as("orderCount");
// 将 BigDecimal 转换为 Decimal128
Decimal128 decimalMinAmount = new Decimal128(minTotalAmount);
MatchOperation matchOperation = Aggregation.match(Criteria.where("totalAmount").gte(decimalMinAmount));
SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.DESC, "totalAmount"));
Aggregation aggregation = Aggregation.newAggregation(groupOperation, matchOperation, sortOperation);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
public List<Document> amountDistribution() {
BucketOperation bucketOperation = Aggregation.bucket("amount")
.withBoundaries(0.0, 100.0, 500.0, 1_000_000_000.0)
.withDefaultBucket("Other")
.andOutput("count").count().as("orderCount")
.andOutput("amount").sum().as("totalAmount");
Aggregation aggregation = Aggregation.newAggregation(bucketOperation);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
public List<Document> qryLastOrderPerUser() {
SortOperation sortOperation = Aggregation.sort(Sort.by(Sort.Direction.DESC, "createTime"));
GroupOperation groupOperation = Aggregation.group("userId")
.first("_id").as("orderId")
.first("amount").as("amount")
.first("status").as("status")
.first("createTime").as("latestTime");
ProjectionOperation projectionOperation = Aggregation.project()
.andExpression("{ $toString: '$_id' }").as("userId")
.andExpression("{ $toString: '$orderId' }").as("orderId")
.and("amount").as("amount")
.and("status").as("status")
.andExpression("{ $dateToString: { format: '%Y-%m-%d %H:%M:%S', date: '$latestTime', timezone: 'Asia/Shanghai' } }").as("latestTime")
.andExclude("_id");
Aggregation aggregation = Aggregation.newAggregation(
sortOperation, groupOperation, projectionOperation
);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
public List<Document> qryOrdersByName(String nameKeyword) {
// 子管道:先过滤用户姓名,再只投影需要的字段
MatchOperation matchStage = Aggregation.match(
Criteria.where("name").regex(nameKeyword, "i")
);
ProjectionOperation projectStage = Aggregation.project("name", "_id");
// $lookup 关联,使用子管道
LookupOperation lookupOperation = LookupOperation.newLookup()
.from("user")
.localField("userId")
.foreignField("_id")
//.pipeline(matchStage, projectStage) //MongoDB 5.0+ / Spring Data MongoDB 3.3+ 才支持的语法
.as("matchedUser");
// $unwind 拆开数组,preserveNullAndEmptyArrays = true 表示没有关联数据时不丢弃主文档
UnwindOperation unwindOperation = Aggregation.unwind("matchedUser", true);
// 确保有关联到用户(非空)
MatchOperation matchOperation = Aggregation.match(Criteria.where("matchedUser").ne(null));
// 订单状态过滤
MatchOperation statusMatch = Aggregation.match(Criteria.where("status").is("PAID"));
// 只返回需要的字段
ProjectionOperation projectionOperation = Aggregation.project()
.andExpression("{ $toString: '$_id' }").as("orderId")
.and("amount").as("amount")
.and("status").as("status")
.and("matchedUser.name").as("userName")
.andExpression("{ $dateToString: { format: '%Y-%m-%d %H:%M:%S', date: '$createTime', timezone: 'Asia/Shanghai' } }").as("createTime")
.andExclude("_id");
Aggregation aggregation = Aggregation.newAggregation(
lookupOperation, unwindOperation, matchOperation, statusMatch, projectionOperation
);
return mongoTemplate.aggregate(aggregation, "order", Document.class).getMappedResults();
}
}
1.5.2 Controller层
java
@RestController
@RequestMapping("/order/agg")
public class OrderAggController {
@Autowired
private OrderAggService orderAggService;
/**
* 按月份分组统计已支付订单的总金额
* 如果输入的参数是字符串,则需要用 @DateTimeFormat 注解转换成 LocalDateTime 对象
* 前端字符串 "2025-01-01 00:00:00"
* ↓ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.NONE)
* Java LocalDateTime 对象
* ↓(mongoTemplate 用这个对象去查询)
* MongoDB 日期查询 { $gte: ISODate(...), $lte: ISODate(...) }
*/
@GetMapping("/sumAmountByMonthForPaidOrders")
public List<Document> sumAmountByMonthForPaidOrders() {
return orderAggService.sumByMonthForPaidOrders();
}
/**
* 查询所有订单信息关联用户姓名
*/
@GetMapping("/qryOrderWithUser")
public List<Document> qryOrderWithUser() {
return orderAggService.qryOrderWithUser();
}
/**
* 根据总金额筛选
*/
@GetMapping("/filterByTotalAmount")
public List<Document> filterByTotalAmount(@RequestParam BigDecimal minAmount) {
return orderAggService.filterUserByTotalAmount(minAmount);
}
/**
* 统计每个区间的订单数量和总金额
*/
@GetMapping("/amountDistribution")
public List<Document> amountDistribution() {
return orderAggService.amountDistribution();
}
/**
* 查询每个用户最新的一条订单信息
*/
@GetMapping("/qryLastOrderPerUser")
public List<Document> qryLastOrderPerUser() {
return orderAggService.qryLastOrderPerUser();
}
/**
* 查询用户姓名包含关键字且订单状态为 PAID
*/
@GetMapping("/qryOrdersByName")
public List<Document> qryOrdersByName(@RequestParam String keyword) {
return orderAggService.qryOrdersByName(keyword);
}
}
二、常用聚合命令详解
| 阶段 | 作用 | 类比 SQL |
|---|---|---|
| $match | 过滤文档 | WHERE |
| $group | 分组聚合 | GROUP BY |
| $sort | 排序 | ORDER BY |
| $project | 字段投影 / 重命名 | SELECT |
| limit/limit/limit/skip | 分页 | LIMIT/OFFSET |
| $lookup | 左连接 | LEFT JOIN |
| $unwind | 拆解数组 | - |
| $bucket | 区间分桶 | CASE WHEN+GROUP BY |
三、高级配置与生产级优化
3.1 索引优化
javascript
db.order.createIndex({ status: 1 });
db.order.createIndex({ createTime: -1 });
db.order.createIndex({ userId: 1 });
db.order.createIndex({ status: 1, createTime: -1 });
3.2 分页优化
-
不推荐:
$skip\+ $limit -
推荐:
$match范围查询 +$limit
3.3 大结果集处理
java
AggregationOptions options = AggregationOptions.builder().allowDiskUse(true).build();
Aggregation aggregation = Aggregation.newAggregation(...).withOptions(options);
3.4 管道优化
-
$match放最前面 -
先过滤后分组
-
避免
$group后用$match
四、常见问题排查
-
聚合结果为空
-
检查
$match条件、集合名、数据类型 -
打印聚合命令,在 Shell 逐级验证
-
-
$lookup 关联失败
- 确保
localField与foreignField类型一致
- 确保
-
查询缓慢
- 建索引、提前过滤、避免大数组
$unwind
- 建索引、提前过滤、避免大数组
五、高频面试题
-
MongoDB 聚合管道执行顺序
$match→$sort→$limit→$skip→$unwind→$group→$project -
聚合性能优化
索引优化、
$match前置、精简返回字段、分片集群 -
聚合分页实现
基础:
$skip\+$limit;优化:范围查询分页 -
project 与 group 区别
-
$group:分组统计,合并文档 -
$project:字段投影,修改文档结构
-