You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:40:09