该示例是基于 MongoDB 聚合框架按省查询省中人口最多和最少的城市。下面增加了额外的排序(sort),以便在不同的 MongoDB 版本中产生稳定的结果。并且演示了分组、排序和投影操作。
下面是示例的关键代码,如下:
(1)映射给定的输入集合的结构
public class ZipInfo { // 城市 private String city; // 省 private String state; // 人口数 private int population; }
(2)投影结果映射的实体
public class ZipInfoStats { // 唯一ID private String id; // 州名称 private String state; // 人口最多城市信息 private City biggestCity; // 人口最少城市信息 private City smallestCity; }
(3)投影相关的关键代码
// 静态导入 import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; TypedAggregation<ZipInfo> aggregation = newAggregation(ZipInfo.class, // 根据 state 和 city 进行分组,统计 population 人口数,并且别名为 pop group("state", "city") .sum("population").as("pop"), // 使用人口数进行排序 sort(ASC, "pop", "state", "city"), // 根据 state(省)分组,取出第一行和最后一行数据,并进行取别名 group("state") .last("city").as("biggestCity") .last("pop").as("biggestPop") .first("city").as("smallestCity") .first("pop").as("smallestPop"), // 对分组结构再次进行投影,结构见 ZipInfoStats project() .and("state").previousOperation() .and("biggestCity") .nested(bind("city", "biggestCity").and("population", "biggestPop")) .and("smallestCity") .nested(bind("city", "smallestCity").and("population", "smallestPop")), sort(ASC, "state") ); AggregationResults<ZipInfoStats> result = mongoTemplate.aggregate(aggregation, City.class, ZipInfoStats.class); List<ZipInfoStats> zipInfoStatsList = result.getMappedResults(); for(ZipInfoStats zipInfoStats : zipInfoStatsList) { System.out.println(JSONObject.toJSONString(zipInfoStats)); }
注意,ZipInfo 类映射了给定的输入集合的结构,ZipInfoStats 类以所需的输出格式定义了该结构。
上面的代码使用了以下算法:
(1)使用 group 操作从输入集合中定义一个组。分组的标准是 state 和 city 字段的组合,这形成了组的 ID 结构。我们通过使用 sum 运算从分组的元素中汇总 population 属性的值,并将结果保存在 pop 字段中。
(2)使用 sort 操作按 pop、state 和 city 字段对中间结果进行升序排序,这样人口最少的城市位于结果的顶部,人口最多的城市位于底部。请注意,对 state 和 city 的排序是隐含地针对组 ID 字段进行的(由 Spring Data MongoDB 处理)。
(3)再次使用 group 操作,将中间结果按 state 分组。请注意,state 再次隐含地引用了一个组 ID 字段。我们在项目操作中分别调用 last(...) 和 first(...) 操作符来选择人口数最多和最少的城市名称。
(4)选择前一个 group 操作中的 state 字段。请注意,state 再次隐含地引用了一个组 ID 字段。因为我们不希望出现一个隐含生成的 ID,所以我们通过使用and(previousOperation()).exclude() 将 ID 从之前的操作中排除。因为我们想在我们的输出类中填充嵌套的城市结构,我们必须通过使用嵌套方法来发出适当的子文档。
(5)在 sort 操作中按 state 对结果列表进行排序。
注意:我们从 newAggregation() 方法的第一个参数 ZipInfo 类派生出输入集合的名称,即调用 mongoTemplate.aggregate() 方法时不需要指定输入集合类型。
(1)application.properties 配置
# Log logging.level.root=debug # MongoDB spring.data.mongodb.uri=mongodb://localhost:27017/test
(2)AppConfig.java 配置类
package com.hxstrive.springdata.mongodb.config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.MongoTemplate; /** * 配置 MongoTemplate * @author hxstrive.com 2022/12/23 */ @Slf4j @Configuration public class AppConfig { @Bean public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory) { log.info("mongoTemplate({}, {})", mongoDatabaseFactory); return new MongoTemplate(mongoDatabaseFactory); } }
(3)映射给定的输入集合的结构
import lombok.Builder; import lombok.Data; /** * city 集合分组后的中间结果实体类型 * @author hxstrive.com */ @Builder @Data public class ZipInfo { // 州名称 private String state; // 城市名称 private String city; // 人口数量 private int population; }
(4)投影结果映射的实体
import lombok.Data; /** * 聚合统计信息实体 * @author hxstrive.com */ @Data public class ZipInfoStats { // 唯一ID private String id; // 州名称 private String state; // 人口最多城市信息 private City biggestCity; // 人口最少城市信息 private City smallestCity; }
(5)客户端代码,通过 MongoTemplate 实现分组和投影。
package com.hxstrive.springdata.mongodb; import com.alibaba.fastjson.JSONObject; import com.hxstrive.springdata.mongodb.entity.demo2.ZipInfo; import com.hxstrive.springdata.mongodb.entity.demo2.ZipInfoStats; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import java.util.List; import static org.springframework.data.domain.Sort.Direction.ASC; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; /** * 聚合框架示例2 * @author hxstrive.com */ @SpringBootTest public class AggregationFrameworkDemo2 { @Autowired private MongoTemplate mongoTemplate; @BeforeEach public void init() { mongoTemplate.dropCollection(ZipInfo.class); // 准备数据 mongoTemplate.insert(ZipInfo.builder().state("SiChuan").city("ChengDu").population(10000).build()); mongoTemplate.insert(ZipInfo.builder().state("SiChuan").city("DaZhou").population(15000).build()); mongoTemplate.insert(ZipInfo.builder().state("GuangZhou").city("ShangHai").population(11000).build()); mongoTemplate.insert(ZipInfo.builder().state("GuangZhou").city("ShenZhen").population(8000).build()); mongoTemplate.insert(ZipInfo.builder().state("JiangSu").city("NanJing").population(10900).build()); } @Test public void contextLoads() { TypedAggregation<ZipInfo> aggregation = newAggregation(ZipInfo.class, group("state", "city") .sum("population").as("pop"), sort(ASC, "pop", "state", "city"), group("state") .last("city").as("biggestCity") .last("pop").as("biggestPop") .first("city").as("smallestCity") .first("pop").as("smallestPop"), project() .and("state").previousOperation() .and("biggestCity") .nested(bind("city", "biggestCity").and("population", "biggestPop")) .and("smallestCity") .nested(bind("city", "smallestCity").and("population", "smallestPop")), sort(ASC, "state") ); AggregationResults<ZipInfoStats> result = mongoTemplate.aggregate(aggregation, ZipInfoStats.class); List<ZipInfoStats> zipInfoStatsList = result.getMappedResults(); for(ZipInfoStats zipInfoStats : zipInfoStatsList) { System.out.println(JSONObject.toJSONString(zipInfoStats)); } // 结果: // {"biggestCity":{"city":"ShangHai","population":11000},"smallestCity":{"city":"ShenZhen","population":8000},"state":"GuangZhou"} // {"biggestCity":{"city":"NanJing","population":10900},"smallestCity":{"city":"NanJing","population":10900},"state":"JiangSu"} // {"biggestCity":{"city":"DaZhou","population":15000},"smallestCity":{"city":"ChengDu","population":10000},"state":"SiChuan"} // 执行的聚合语句如下: // [ // { "$group" : { "_id" : { "state" : "$state", "city" : "$city"}, "pop" : { "$sum" : "$population"}}}, // { "$sort" : { "pop" : 1, "_id.state" : 1, "_id.city" : 1}}, // { "$group" : { "_id" : "$_id.state", // "biggestCity" : { "$last" : "$_id.city"}, "biggestPop" : { "$last" : "$pop"}, // "smallestCity" : { "$first" : "$_id.city"}, "smallestPop" : { "$first" : "$pop"}}}, // { "$project" : { "_id" : 0, "state" : "$_id", // "biggestCity" : { "city" : "$biggestCity", "population" : "$biggestPop"}, // "smallestCity" : { "city" : "$smallestCity", "population" : "$smallestPop"}}}, // { "$sort" : { "state" : 1}} // ] } }