一、前言
在实际的企业级应用开发中,对MongoDB数据进行复杂查询是常见的需求。Spring Data MongoDB提供的MongoTemplate是一个功能强大的工具,能够帮助我们执行各种复杂的查询操作。本文将详细讲解如何使用MongoTemplate实现MongoDB的分组、排序、分页和连表查询。
二、环境准备
2.1 依赖配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2.2 实体类定义
假设我们有两个集合:用户(users)和订单(orders)。
java
@Data
@Document(collection = "users")
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private String id;
private String username;
private String email;
private Integer age;
private String city;
private Date createTime;
}
@Data
@Document(collection = "orders")
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@Id
private String id;
private String userId;
private String orderNo;
private Double amount;
private Integer status; // 1:待支付 2:已支付 3:已完成 4:已取消
private Date createTime;
private List<OrderItem> items;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class OrderItem {
private String productId;
private String productName;
private Integer quantity;
private Double price;
}
}
三、基础查询操作
3.1 简单查询
java
@Service
public class MongoQueryService {
@Autowired
private MongoTemplate mongoTemplate;
// 查询所有用户
public List<User> findAllUsers() {
return mongoTemplate.findAll(User.class);
}
// 条件查询
public List<User> findUsersByCity(String city) {
Query query = Query.query(Criteria.where("city").is(city));
return mongoTemplate.find(query, User.class);
}
// 范围查询
public List<User> findUsersByAgeRange(int minAge, int maxAge) {
Query query = Query.query(
Criteria.where("age").gte(minAge).lte(maxAge)
);
return mongoTemplate.find(query, User.class);
}
}
四、分组查询(Aggregation)
4.1 基础分组统计
java
@Service
public class AggregationService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 按城市分组统计用户数量
*/
public Map<String, Long> groupUsersByCity() {
// 创建聚合操作
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.group("city").count().as("count"),
Aggregation.project("count").and("city").previousOperation(),
Aggregation.sort(Sort.Direction.DESC, "count")
);
// 执行聚合查询
AggregationResults<Document> results =
mongoTemplate.aggregate(aggregation, "users", Document.class);
// 转换为Map
return results.getMappedResults().stream()
.collect(Collectors.toMap(
doc -> doc.getString("city"),
doc -> doc.getLong("count")
));
}
/**
* 按年龄段分组统计
*/
public List<Document> groupUsersByAgeGroup() {
// 定义年龄阶段
AggregationOperation ageGroup = context -> new Document("$addFields",
new Document("ageGroup",
new Document("$switch",
new Document("branches", Arrays.asList(
new Document("case",
new Document("$lt", Arrays.asList("$age", 18)))
.append("then", "未成年"),
new Document("case",
new Document("$and", Arrays.asList(
new Document("$gte", Arrays.asList("$age", 18)),
new Document("$lte", Arrays.asList("$age", 30))
)))
.append("then", "青年"),
new Document("case",
new Document("$and", Arrays.asList(
new Document("$gt", Arrays.asList("$age", 30)),
new Document("$lte", Arrays.asList("$age", 50))
)))
.append("then", "中年"),
new Document("case",
new Document("$gt", Arrays.asList("$age", 50)))
.append("then", "老年")
))
.append("default", "未知")
)
)
);
Aggregation aggregation = Aggregation.newAggregation(
ageGroup,
Aggregation.group("ageGroup")
.count().as("count")
.avg("age").as("avgAge"),
Aggregation.sort(Sort.Direction.DESC, "count")
);
return mongoTemplate.aggregate(aggregation, "users", Document.class)
.getMappedResults();
}
}
4.2 多字段分组与复杂统计
java
@Service
public class ComplexAggregationService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 按城市和状态分组统计订单
*/
public List<OrderStats> groupOrdersByCityAndStatus() {
// 使用$lookup进行关联查询
LookupOperation lookup = LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userInfo");
// 展开关联的用户信息
UnwindOperation unwind = Aggregation.unwind("userInfo");
Aggregation aggregation = Aggregation.newAggregation(
lookup,
unwind,
Aggregation.group("userInfo.city", "status")
.count().as("orderCount")
.sum("amount").as("totalAmount")
.avg("amount").as("avgAmount"),
Aggregation.project()
.and("_id.city").as("city")
.and("_id.status").as("status")
.and("orderCount").as("orderCount")
.and("totalAmount").as("totalAmount")
.and("avgAmount").as("avgAmount"),
Aggregation.sort(Sort.Direction.DESC, "totalAmount")
);
return mongoTemplate.aggregate(aggregation, "orders", OrderStats.class)
.getMappedResults();
}
/**
* 统计DTO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class OrderStats {
private String city;
private Integer status;
private Long orderCount;
private Double totalAmount;
private Double avgAmount;
}
/**
* 使用日期分组统计
*/
public List<Document> groupOrdersByDate() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.project()
.and("amount").as("amount")
.and(DateOperators.dateOf("createTime")
.toString("%Y-%m-%d")).as("date"),
Aggregation.group("date")
.count().as("count")
.sum("amount").as("totalAmount")
.push("amount").as("amounts"),
Aggregation.project()
.and("_id").as("date")
.and("count").as("count")
.and("totalAmount").as("totalAmount")
.and("amounts").as("amounts")
.and(ArrayOperators.arrayOf("amounts")
.max()).as("maxAmount")
.and(ArrayOperators.arrayOf("amounts")
.min()).as("minAmount"),
Aggregation.sort(Sort.Direction.ASC, "date")
);
return mongoTemplate.aggregate(aggregation, "orders", Document.class)
.getMappedResults();
}
}
五、排序查询
5.1 单字段排序
java
@Service
public class SortService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 单字段排序
*/
public List<User> findUsersSorted(String field, boolean ascending) {
Sort.Direction direction = ascending ?
Sort.Direction.ASC : Sort.Direction.DESC;
Query query = new Query()
.with(Sort.by(direction, field));
return mongoTemplate.find(query, User.class);
}
/**
* 多字段排序
*/
public List<User> findUsersMultiSorted() {
Query query = new Query()
.with(Sort.by(
Sort.Order.desc("age"),
Sort.Order.asc("createTime")
));
return mongoTemplate.find(query, User.class);
}
}
5.2 聚合查询中的排序
java
@Service
public class AggregationSortService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 聚合结果排序
*/
public List<Document> findTopCitiesByOrderAmount(int limit) {
LookupOperation lookup = LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userInfo");
Aggregation aggregation = Aggregation.newAggregation(
lookup,
Aggregation.unwind("userInfo"),
Aggregation.group("userInfo.city")
.sum("amount").as("totalAmount")
.count().as("orderCount"),
Aggregation.project()
.and("_id").as("city")
.and("totalAmount").as("totalAmount")
.and("orderCount").as("orderCount")
.andExpression("totalAmount / orderCount").as("avgAmount"),
Aggregation.sort(Sort.Direction.DESC, "totalAmount"),
Aggregation.limit(limit)
);
return mongoTemplate.aggregate(aggregation, "orders", Document.class)
.getMappedResults();
}
}
六、分页查询
6.1 基础分页
java
@Service
public class PaginationService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 基础分页查询
*/
public Page<User> findUsersByPage(int page, int size, String sortField, boolean ascending) {
// 构建查询条件
Query query = new Query();
// 排序
Sort.Direction direction = ascending ?
Sort.Direction.ASC : Sort.Direction.DESC;
query.with(Sort.by(direction, sortField));
// 计算总数
long total = mongoTemplate.count(query, User.class);
// 分页
query.skip((long) (page - 1) * size).limit(size);
// 查询数据
List<User> content = mongoTemplate.find(query, User.class);
return new PageImpl<>(content, PageRequest.of(page - 1, size), total);
}
/**
* 分页查询DTO
*/
@Data
@AllArgsConstructor
public static class PageResult<T> {
private List<T> content;
private int currentPage;
private int pageSize;
private long totalElements;
private int totalPages;
public PageResult(Page<T> page) {
this.content = page.getContent();
this.currentPage = page.getNumber() + 1;
this.pageSize = page.getSize();
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
}
}
}
6.2 聚合分页查询
java
@Service
public class AggregationPaginationService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 聚合查询分页
*/
public PageResult<Document> aggregateWithPagination(
int page, int size, String city) {
// 构建聚合管道
List<AggregationOperation> operations = new ArrayList<>();
// 条件过滤
if (StringUtils.isNotBlank(city)) {
operations.add(Aggregation.match(Criteria.where("city").is(city)));
}
// 添加分页操作前先获取总数
Aggregation countAggregation = Aggregation.newAggregation(
Aggregation.group().count().as("total")
);
// 添加其他聚合操作
operations.addAll(Arrays.asList(
Aggregation.group("age")
.count().as("count")
.avg("age").as("avgAge"),
Aggregation.sort(Sort.Direction.DESC, "count"),
Aggregation.skip((long) (page - 1) * size),
Aggregation.limit(size)
));
// 执行数据查询
Aggregation dataAggregation = Aggregation.newAggregation(operations);
List<Document> content = mongoTemplate
.aggregate(dataAggregation, "users", Document.class)
.getMappedResults();
// 执行计数查询
Document countResult = mongoTemplate
.aggregate(countAggregation, "users", Document.class)
.getUniqueMappedResult();
long total = countResult != null ? countResult.getLong("total") : 0;
// 计算总页数
int totalPages = (int) Math.ceil((double) total / size);
return new PageResult<>(content, page, size, total, totalPages);
}
/**
* 使用facet实现高效分页(MongoDB 3.4+)
*/
public Map<String, Object> facetPagination(int page, int size) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.facet(
// 分页数据
Aggregation.skip((long) (page - 1) * size),
Aggregation.limit(size)
).as("data")
.and(
// 统计信息
Aggregation.count().as("total")
).as("metadata")
);
return mongoTemplate.aggregate(aggregation, "users", Document.class)
.getUniqueMappedResult();
}
}
七、连表查询($lookup)
7.1 基础连表查询
java
@Service
public class LookupService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 一对一关联查询
*/
public List<OrderWithUser> findOrdersWithUserInfo() {
LookupOperation lookup = LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userInfo");
// 由于是一对一,使用$unwind展开数组
UnwindOperation unwind = Aggregation.unwind("userInfo", true);
Aggregation aggregation = Aggregation.newAggregation(
lookup,
unwind,
Aggregation.project()
.and("id").as("orderId")
.and("orderNo").as("orderNo")
.and("amount").as("amount")
.and("status").as("status")
.and("userInfo.username").as("username")
.and("userInfo.email").as("email")
.and("userInfo.city").as("city")
);
return mongoTemplate.aggregate(aggregation, "orders", OrderWithUser.class)
.getMappedResults();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class OrderWithUser {
private String orderId;
private String orderNo;
private Double amount;
private Integer status;
private String username;
private String email;
private String city;
}
}
7.2 多表关联与复杂查询
java
@Service
public class ComplexLookupService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 多条件关联查询
*/
public List<Document> findOrdersWithUserDetails(String city, Date startDate) {
// 定义匹配条件
Criteria userCriteria = Criteria.where("city").is(city);
Criteria orderCriteria = Criteria.where("createTime").gte(startDate);
LookupOperation lookup = LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userDetails")
.let(OperationsEvaluator.LetOperations.newLetOperations()
.addVariable("orderUserId", "$userId"))
.pipeline(Aggregation.newAggregation(
Aggregation.match(
Criteria.where("_id").is("$$orderUserId")
.and("city").is(city)
)
));
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(orderCriteria),
lookup,
Aggregation.unwind("userDetails", true),
Aggregation.match(Criteria.where("userDetails").ne(null)),
Aggregation.project()
.and("orderNo").as("orderNo")
.and("amount").as("amount")
.and("createTime").as("createTime")
.and("userDetails.username").as("username")
.and("userDetails.city").as("userCity")
.and(ArrayOperators.arrayOf("items").length()).as("itemCount"),
Aggregation.sort(Sort.Direction.DESC, "createTime")
);
return mongoTemplate.aggregate(aggregation, "orders", Document.class)
.getMappedResults();
}
/**
* 嵌套数组查询
*/
public List<Document> findOrdersWithProductInfo() {
// 模拟产品表关联(实际中应该有products集合)
LookupOperation productLookup = LookupOperation.newLookup()
.from("products") // 假设有products集合
.localField("items.productId")
.foreignField("_id")
.as("productInfo");
// 添加字段处理
AddFieldsOperation addFields = Aggregation.addFields()
.addFieldWithValue("itemsWithProduct", new Document("$map",
new Document("input", "$items")
.append("as", "item")
.append("in", new Document()
.append("productId", "$$item.productId")
.append("productName", "$$item.productName")
.append("quantity", "$$item.quantity")
.append("price", "$$item.price")
.append("subTotal", new Document("$multiply",
Arrays.asList("$$item.quantity", "$$item.price")))
)
)).build();
Aggregation aggregation = Aggregation.newAggregation(
addFields,
Aggregation.unwind("itemsWithProduct"),
Aggregation.group("orderNo")
.first("userId").as("userId")
.first("amount").as("totalAmount")
.push("itemsWithProduct").as("orderItems")
.sum("itemsWithProduct.subTotal").as("calculatedTotal"),
Aggregation.match(Criteria.where("calculatedTotal")
.gte(1000)),
Aggregation.sort(Sort.Direction.DESC, "calculatedTotal")
);
return mongoTemplate.aggregate(aggregation, "orders", Document.class)
.getMappedResults();
}
}
7.3 多层嵌套关联
java
@Service
public class NestedLookupService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 多层关联查询示例
*/
public List<Document> findCompleteOrderInfo() {
// 第一层:关联用户信息
LookupOperation userLookup = LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userInfo");
// 第二层:关联产品信息(假设有products集合)
LookupOperation productLookup = LookupOperation.newLookup()
.from("products")
.localField("items.productId")
.foreignField("_id")
.as("productDetails");
// 第三层:关联物流信息(假设有logistics集合)
LookupOperation logisticsLookup = LookupOperation.newLookup()
.from("logistics")
.localField("orderNo")
.foreignField("orderNo")
.as("logisticsInfo");
Aggregation aggregation = Aggregation.newAggregation(
userLookup,
Aggregation.unwind("userInfo", true),
productLookup,
logisticsLookup,
Aggregation.unwind("logisticsInfo", true),
Aggregation.addFields()
.addFieldWithValue("orderDate",
DateOperators.dateOf("createTime").toString("%Y-%m-%d"))
.build(),
Aggregation.project()
.andInclude("orderNo", "amount", "status")
.and("userInfo.username").as("customerName")
.and("userInfo.phone").as("customerPhone")
.and("userInfo.address").as("deliveryAddress")
.and("orderDate").as("orderDate")
.and("logisticsInfo.trackingNo").as("trackingNumber")
.and("logisticsInfo.status").as("logisticsStatus")
.and("items").as("orderItems")
.and("productDetails").as("productInfo"),
Aggregation.match(Criteria.where("status").is(2)), // 只查询已支付订单
Aggregation.sort(Sort.Direction.DESC, "createTime")
);
return mongoTemplate.aggregate(aggregation, "orders", Document.class)
.getMappedResults();
}
}
八、性能优化技巧
8.1 索引优化
java
@Configuration
public class MongoIndexConfig {
@Autowired
private MongoTemplate mongoTemplate;
@PostConstruct
public void createIndexes() {
// 创建单字段索引
mongoTemplate.indexOps(User.class).ensureIndex(
new Index().on("city", Sort.Direction.ASC)
);
// 创建复合索引
mongoTemplate.indexOps(User.class).ensureIndex(
new Index().on("city", Sort.Direction.ASC)
.on("age", Sort.Direction.DESC)
);
// 创建TTL索引(自动过期)
mongoTemplate.indexOps(Order.class).ensureIndex(
new Index().on("createTime", Sort.Direction.ASC)
.expire(30, TimeUnit.DAYS) // 30天后自动删除
);
// 创建文本索引
mongoTemplate.indexOps(User.class).ensureIndex(
new Index().on("username", Sort.Direction.ASC)
.named("username_text_idx")
);
}
}
8.2 查询优化
java
@Service
public class QueryOptimizationService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 使用投影减少返回字段
*/
public List<Document> findUsersWithProjection() {
Query query = new Query();
query.fields()
.include("username")
.include("email")
.include("city")
.exclude("id");
return mongoTemplate.find(query, Document.class, "users");
}
/**
* 使用 hint 强制使用索引
*/
public List<User> findUsersWithIndexHint() {
Query query = Query.query(Criteria.where("city").is("北京"));
query.withHint("city_1_age_-1"); // 使用指定的索引
return mongoTemplate.find(query, User.class);
}
/**
* 批量查询优化
*/
public Map<String, User> batchFindUsers(List<String> userIds) {
// 使用 in 查询而不是循环查询
Query query = Query.query(Criteria.where("id").in(userIds));
List<User> users = mongoTemplate.find(query, User.class);
return users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
}
}
九、完整示例:综合查询服务
java
@Service
@Slf4j
public class ComprehensiveQueryService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 综合查询:分组 + 排序 + 分页 + 关联
*/
public PageResult<OrderSummaryDTO> comprehensiveQuery(
OrderQueryParam param) {
// 1. 构建聚合管道
List<AggregationOperation> operations = buildAggregationPipeline(param);
// 2. 添加分页
operations.add(Aggregation.skip((long) (param.getPage() - 1) * param.getSize()));
operations.add(Aggregation.limit(param.getSize()));
// 3. 执行数据查询
Aggregation dataAggregation = Aggregation.newAggregation(operations);
List<OrderSummaryDTO> content = mongoTemplate
.aggregate(dataAggregation, "orders", OrderSummaryDTO.class)
.getMappedResults();
// 4. 执行总数查询
long total = countTotal(param);
// 5. 构建返回结果
int totalPages = (int) Math.ceil((double) total / param.getSize());
return new PageResult<>(content, param.getPage(), param.getSize(), total, totalPages);
}
private List<AggregationOperation> buildAggregationPipeline(OrderQueryParam param) {
List<AggregationOperation> operations = new ArrayList<>();
// 条件过滤
Criteria criteria = buildCriteria(param);
if (criteria != null) {
operations.add(Aggregation.match(criteria));
}
// 关联用户表
operations.add(buildLookupOperation());
operations.add(Aggregation.unwind("userInfo", true));
// 分组统计
operations.add(Aggregation.group("userInfo.city", "status")
.count().as("orderCount")
.sum("amount").as("totalAmount")
.avg("amount").as("avgAmount"));
// 投影
operations.add(Aggregation.project()
.and("_id.city").as("city")
.and("_id.status").as("status")
.and("orderCount").as("orderCount")
.and("totalAmount").as("totalAmount")
.and("avgAmount").as("avgAmount")
.andExclude("_id"));
// 排序
if (StringUtils.isNotBlank(param.getSortField())) {
operations.add(Aggregation.sort(
param.getSortOrder() == 1 ?
Sort.Direction.ASC : Sort.Direction.DESC,
param.getSortField()
));
} else {
operations.add(Aggregation.sort(Sort.Direction.DESC, "totalAmount"));
}
return operations;
}
private Criteria buildCriteria(OrderQueryParam param) {
Criteria criteria = new Criteria();
List<Criteria> criteriaList = new ArrayList<>();
if (StringUtils.isNotBlank(param.getCity())) {
criteriaList.add(Criteria.where("userInfo.city").is(param.getCity()));
}
if (param.getStatus() != null) {
criteriaList.add(Criteria.where("status").is(param.getStatus()));
}
if (param.getStartDate() != null && param.getEndDate() != null) {
criteriaList.add(Criteria.where("createTime")
.gte(param.getStartDate()).lte(param.getEndDate()));
}
if (param.getMinAmount() != null) {
criteriaList.add(Criteria.where("amount").gte(param.getMinAmount()));
}
if (param.getMaxAmount() != null) {
criteriaList.add(Criteria.where("amount").lte(param.getMaxAmount()));
}
return criteriaList.isEmpty() ? null : criteria.andOperator(criteriaList.toArray(new Criteria[0]));
}
private LookupOperation buildLookupOperation() {
return LookupOperation.newLookup()
.from("users")
.localField("userId")
.foreignField("_id")
.as("userInfo");
}
private long countTotal(OrderQueryParam param) {
List<AggregationOperation> countOperations = new ArrayList<>();
Criteria criteria = buildCriteria(param);
if (criteria != null) {
countOperations.add(Aggregation.match(criteria));
}
countOperations.add(buildLookupOperation());
countOperations.add(Aggregation.unwind("userInfo", true));
countOperations.add(Aggregation.group("userInfo.city", "status"));
countOperations.add(Aggregation.group().count().as("total"));
Aggregation countAggregation = Aggregation.newAggregation(countOperations);
Document countResult = mongoTemplate
.aggregate(countAggregation, "orders", Document.class)
.getUniqueMappedResult();
return countResult != null ? countResult.getLong("total") : 0;
}
@Data
public static class OrderQueryParam {
private Integer page = 1;
private Integer size = 20;
private String city;
private Integer status;
private Date startDate;
private Date endDate;
private Double minAmount;
private Double maxAmount;
private String sortField = "totalAmount";
private Integer sortOrder = -1; // -1: desc, 1: asc
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class OrderSummaryDTO {
private String city;
private Integer status;
private Long orderCount;
private Double totalAmount;
private Double avgAmount;
}
}
十、最佳实践与注意事项
10.1 性能最佳实践
- 合理使用索引:为频繁查询的字段创建索引
- 避免全表扫描:尽量使用索引字段进行查询
- 限制返回字段:使用投影减少数据传输
- 分页查询优化:避免使用skip进行深度分页
- 批量操作:使用bulkWrite进行批量操作
10.2 代码组织建议
- 使用Repository模式:将数据访问逻辑封装在Repository中
- DTO转换:使用DTO进行数据传输,避免暴露实体类
- 异常处理:统一处理MongoDB异常
- 日志记录:记录关键操作的执行时间和结果
- 监控告警:监控慢查询和异常查询