Spring Mongo聚合按carId分组取min值并保留所有字段的方案
需求与问题
需要获取每个carId对应车辆的最快速度(即raceResulttime最小)的完整记录。当前使用Spring Mongo聚合框架按carId分组并取raceResulttime最小值后,返回结果仅保留carId和raceResulttime,其他字段均为null。尝试过的方案要么将字段存入数组,要么无法保证carId唯一。
集合数据
Race{raceId='64c2350402708c36b821e5bc', raceNum=430, trackNum='2', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=2.262371, raceDatetime=Thu Jul 27 17:12:36 HKT 2023, venue='HK', rank='2', isDnf=false, raceSpeed=0.0}, Race{raceId='64c2355f02708c36b821e5bf', raceNum=430, trackNum='2', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=2.262371, raceDatetime=Thu Jul 27 17:14:07 HKT 2023, venue='HK', rank='2', isDnf=false, raceSpeed=0.0}, Race{raceId='64c2357002708c36b821e5c2', raceNum=431, trackNum='2', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=2.262371, raceDatetime=Thu Jul 27 17:14:24 HKT 2023, venue='HK', rank='2', isDnf=false, raceSpeed=0.0}, Race{raceId='64c2357e02708c36b821e5c5', raceNum=432, trackNum='2', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=2.262371, raceDatetime=Thu Jul 27 17:14:38 HKT 2023, venue='HK', rank='2', isDnf=false, raceSpeed=0.0}, Race{raceId='64c2358d02708c36b821e5c8', raceNum=433, trackNum='2', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=2.262371, raceDatetime=Thu Jul 27 17:14:53 HKT 2023, venue='HK', rank='2', isDnf=false, raceSpeed=0.0}, Race{raceId='64c23282bc05ab7f8acd7c2a', raceNum=421, trackNum='1', carUser='null', carId='ZZ9991', raceStart=null, raceEnd=null, raceResulttime=4.298986, raceDatetime=Thu Jul 27 17:01:54 HKT 2023, venue='HK', rank='3', isDnf=false, raceSpeed=0.8806175}, Race{raceId='64c23294bc05ab7f8acd7c2c', raceNum=422, trackNum='1', carUser='null', carId='ZZ9991', raceStart=null, raceEnd=null, raceResulttime=4.298986, raceDatetime=Thu Jul 27 17:02:12 HKT 2023, venue='HK', rank='3', isDnf=false, raceSpeed=0.8806175}
现有聚合代码
@Override public void groupByField() { // Match stage MatchOperation matchStage = Aggregation.match(new Criteria("raceResulttime").gt(0) .and("isDnf").is(false) .and("raceNum").gt(0) ); GroupOperation groupStage = group("carId") .min("raceResulttime").as("raceResulttime"); SortOperation sortStage = Aggregation.sort(Sort.Direction.ASC, "raceResulttime"); ProjectionOperation projectionOperation = Aggregation.project("raceResulttime","rank","raceSpeed","raceNum").and("carId").previousOperation(); // Aggregation Aggregation aggregation = newAggregation(matchStage, groupStage, sortStage,projectionOperation); AggregationResults<Race> resultss = mongoTemplate.aggregate(aggregation, "race", Race.class); for ( Race result : resultss) System.out.println("4 - resultss - "+ result); System.out.println("5" + resultss.getMappedResults()); }
当前返回结果
- resultss - Race{raceId='null', raceNum=null, trackNum='null', carUser='null', carId='ZZ9992', raceStart=null, raceEnd=null, raceResulttime=1.243837, raceDatetime=null, venue='null', rank='null', isDnf=false, raceSpeed=null} 4 - resultss - Race{raceId='null', raceNum=null, trackNum='null', carUser='null', carId='ZZ9991', raceStart=null, raceEnd=null, raceResulttime=1.391792, raceDatetime=null, venue='null', rank='null', isDnf=false, raceSpeed=null} 4 - resultss - Race{raceId='null', raceNum=null, trackNum='null', carUser='null', carId='ZZ9990', raceStart=null, raceEnd=null, raceResulttime=1.483507, raceDatetime=null, venue='null', rank='null', isDnf=false, raceSpeed=null} 4 - resultss - Race{raceId='null', raceNum=null, trackNum='null', carUser='null', carId='ZZ9995', raceStart=null, raceEnd=null, raceResulttime=1.493411, raceDatetime=null, venue='null', rank='null', isDnf=false, raceSpeed=null} 4 - resultss - Race{raceId='null', raceNum=null, trackNum='null', carUser='null', carId='ZZ9996', raceStart=null, raceEnd=null, raceResulttime=1.661812, raceDatetime=null, venue='null', rank='null', isDnf=false, raceSpeed=null}
解决方案
核心思路:先按carId和raceResulttime升序排序,确保每组内最快的记录排在首位;再按carId分组,取每组第一条完整记录,即可保留该车辆最快速度对应的所有字段。
修改后的聚合代码
@Override public void groupByField() { // 过滤有效记录 MatchOperation matchStage = Aggregation.match(new Criteria("raceResulttime").gt(0) .and("isDnf").is(false) .and("raceNum").gt(0) ); // 先按carId分组排序,再按raceResulttime升序(最快记录排前面) SortOperation sortStage = Aggregation.sort(Sort.Direction.ASC, "carId", "raceResulttime"); // 按carId分组,取每组第一条完整记录 GroupOperation groupStage = Aggregation.group("carId") .first(Aggregation.ROOT).as("fastestRace"); // 提取完整的Race对象,排除自动生成的_id ProjectionOperation projectionStage = Aggregation.project("fastestRace") .andExclude("_id"); // 构建聚合管道 Aggregation aggregation = Aggregation.newAggregation(matchStage, sortStage, groupStage, projectionStage); // 执行聚合,先获取Document结果 AggregationResults<Document> results = mongoTemplate.aggregate(aggregation, "race", Document.class); // 将Document转换为Race对象 List<Race> fastestRaces = results.getMappedResults().stream() .map(doc -> mongoTemplate.getConverter().read(Race.class, doc.get("fastestRace", Document.class))) .collect(Collectors.toList()); // 输出结果 for (Race race : fastestRaces) { System.out.println("最快记录:" + race); } }
效果说明
修改后,每个carId仅返回一条记录,包含该车辆最快速度对应的所有字段(raceId、raceNum、trackNum等),且保证carId唯一,完全符合需求。
内容的提问来源于stack exchange,提问作者Man Man Yu
相关产品推荐
相关产品推荐

