MongoDB按年月获取文档:是否可无需聚合实现(Spring Boot环境)
在MongoDB中按年月处理文档的方案(结合Spring Data MongoDB)
嘿,我来帮你捋捋这个问题~首先直接给你明确结论:
一、不用aggregate能不能实现?分两种情况看
- 如果你的需求是筛选出某一个特定年月的所有文档(比如只拿2023年10月的记录),完全不用聚合!用基础的
find查询就能搞定,和SQL里的WHERE筛选逻辑类似。 - 如果你的需求是按年月分组汇总(比如统计每个年月有多少文档、每个年月的总金额这类),那必须用聚合(或者你把所有数据拉到应用层自己分组,但这只适合小数据量)。因为MongoDB的基础查询没法直接做分组操作,聚合就是干这个事儿的~
二、用聚合实现这个需求恰当吗?当然恰当!
你完全不用怀疑这点~聚合是MongoDB专门用来处理分组、统计、数据变换这类场景的核心功能,尤其是数据量较大的时候,在数据库层做聚合比把所有数据拉到应用层处理效率高太多,这绝对是正确的选择,你的担心是多余的😉
三、结合Spring Boot + Spring Data MongoDB的具体实现
下面给你分场景给出最简便的实现方式:
场景1:筛选特定年月的文档(不用聚合)
假设你的实体类YourDocument里有一个LocalDateTime类型的字段createTime,用来记录文档创建时间:
方式1:利用Spring Data的查询方法命名规则
直接在Repository接口里定义方法,Spring Data会自动帮你生成查询语句:
import org.springframework.data.mongodb.repository.MongoRepository; import java.time.LocalDateTime; import java.util.List; public interface YourDocumentRepository extends MongoRepository<YourDocument, String> { // 筛选createTime在[start, end)区间的文档 List<YourDocument> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end); }
调用的时候只需要构造当月的起始和结束时间:
// 比如查询2023年10月的文档 LocalDateTime startOfMonth = LocalDateTime.of(2023, 10, 1, 0, 0, 0); // 结束时间设为下一个月的0点,这样就能包含当月最后一秒的记录 LocalDateTime endOfMonth = startOfMonth.plusMonths(1); List<YourDocument> octoberDocs = yourDocumentRepository.findByCreateTimeBetween(startOfMonth, endOfMonth);
方式2:用Criteria动态构建查询
如果需要更灵活的查询条件(比如结合其他筛选),可以用MongoTemplate:
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; // 注入MongoTemplate @Autowired private MongoTemplate mongoTemplate; // 构建查询 Query query = new Query(); query.addCriteria(Criteria.where("createTime").gte(startOfMonth).lt(endOfMonth)); List<YourDocument> octoberDocs = mongoTemplate.find(query, YourDocument.class);
场景2:按年月分组汇总(用聚合,类似SQL的GROUP BY)
比如统计每个年月的文档数量,我们可以用Spring Data提供的AggregationAPI或者@Aggregation注解:
方式1:用Aggregation API构建管道
先定义一个用来接收聚合结果的DTO:
public class YearMonthStats { private int year; private int month; private long docCount; // 别忘了加getter和setter public int getYear() { return year; } public void setYear(int year) { this.year = year; } public int getMonth() { return month; } public void setMonth(int month) { this.month = month; } public long getDocCount() { return docCount; } public void setDocCount(long docCount) { this.docCount = docCount; } }
然后用聚合管道实现分组统计:
import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.domain.Sort; @Autowired private MongoTemplate mongoTemplate; public List<YearMonthStats> getYearMonthStats() { Aggregation aggregation = Aggregation.newAggregation( // 第一步:从createTime中提取年和月,生成新字段 Aggregation.project() .andExpression("year(createTime)").as("year") .andExpression("month(createTime)").as("month"), // 第二步:按年和月分组,统计每组的文档数量 Aggregation.group("year", "month") .count().as("docCount"), // 可选:按年月倒序排列,最新的在前 Aggregation.sort(Sort.Direction.DESC, "year", "month") ); // 执行聚合,指定集合名和结果DTO类型 AggregationResults<YearMonthStats> results = mongoTemplate.aggregate( aggregation, "your_collection_name", YearMonthStats.class ); return results.getMappedResults(); }
方式2:用@Aggregation注解简化代码
直接在Repository接口上写聚合管道的JSON语句,更简洁:
import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.MongoRepository; import java.util.List; public interface YourDocumentRepository extends MongoRepository<YourDocument, String> { @Aggregation(pipeline = { "{$project: {year: {$year: '$createTime'}, month: {$month: '$createTime'}}}", "{$group: {_id: {year: '$year', month: '$month'}, docCount: {$sum: 1}}}", "{$sort: {'_id.year': -1, '_id.month': -1}}" }) List<YearMonthStats> countDocumentsByYearMonth(); }
这里注意DTO的字段要和聚合结果对应,因为分组后的_id是包含year和month的对象,所以可以给DTO的字段加@Field注解映射:
import org.springframework.data.mongodb.core.mapping.Field; public class YearMonthStats { @Field("_id.year") private int year; @Field("_id.month") private int month; private long docCount; // getter和setter }
场景3:小数据量下的应用层分组(备选方案)
如果你的数据量很小,也可以先查询所有文档,然后用Java的Stream API在应用层分组:
import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.time.LocalDateTime; List<YourDocument> allDocs = yourDocumentRepository.findAll(); // 按"yyyy-MM"格式分组,键是年月字符串,值是对应月份的文档列表 Map<String, List<YourDocument>> groupedByYearMonth = allDocs.stream() .collect(Collectors.groupingBy(doc -> { LocalDateTime createTime = doc.getCreateTime(); return String.format("%d-%02d", createTime.getYear(), createTime.getMonthValue()); }));
但强烈不推荐在数据量较大时用这种方式,会占用大量内存和带宽,效率极低。
总结
- 单纯筛选某年月的文档:用基础查询就够了,Spring Data提供了非常简便的实现
- 按年月分组统计:用聚合是最恰当、最高效的方式,完全符合MongoDB的设计理念
- 应用层分组只适合小数据量的临时场景
内容的提问来源于stack exchange,提问作者peterzinho16
相关产品推荐
相关产品推荐

