Spring Data MongoDB 是 Spring 生态中对 MongoDB 的官方支持,提供了 Repository 接口和 MongoTemplate 两种操作模式。本文覆盖配置、基础 CRUD、复杂查询、聚合、事务、实时监听、大文件存储、地理空间查询、全文检索以及索引优化,帮助开发者构建生产级 MongoDB 应用。
1. 配置
Spring Boot 通过 spring-boot-starter-data-mongodb 提供开箱即用的 MongoDB 集成。推荐使用 URI 方式连接,可一次性指定副本集、读写偏好、连接池大小以及认证信息。此外,MongoDB 4.2+ 建议使用事务,因此连接字符串中的副本集参数 replicaSet=rs0 是必需的,因为 MongoDB 的事务必须运行在副本集或分片集群上。
# application.yml
spring:
data:
mongodb:
uri: mongodb://user:pass@mongo1:27017,mongo2:27017/mydatabase?replicaSet=rs0&readPreference=primaryPreferred&w=majority
# 可选连接池参数
# connection-pool:
# min-size: 10
# max-size: 200
# max-wait-time: 2000
# max-connection-life-time: 0
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
w=majority 确保写入操作在大多数节点提交后才返回成功,保障数据一致性。readPreference=primaryPreferred 让读请求优先走主节点,主节点不可用时自动切换到从节点,兼顾一致性和可用性。连接池大小应根据并发量调整,避免连接耗尽导致服务降级。
2. 实体映射
Spring Data MongoDB 使用一套丰富的注解完成 Java 对象与 MongoDB 文档的双向映射。@Document 指定集合名,@Id 映射 _id 字段,@Field 自定义字段名,@DBRef 建立引用关系。对于嵌套文档,直接使用 Java 对象即可,MongoDB 驱动会自动完成序列化与反序列化。@CompoundIndex 可以在类级别声明复合索引,提升查询效率。
@Document(collection = "products")
@CompoundIndex(name = "category_price", def = "{'category': 1, 'price': -1}")
@CompoundIndex(name = "name_text", def = "{'name': 'text'}")
public class Product {
@Id
private String id;
private String name;
private String category;
private BigDecimal price;
private int stock;
@Field("created_at")
private Instant createdAt;
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private GeoJsonPoint location;
@DBRef
private Supplier supplier;
private List<Review> reviews;
private Map<String, String> attributes;
@Version
private Long version;
}
public record Review(String userId, int rating, String comment, Instant date) {}
@Version 字段为乐观锁机制提供支持,当并发的更新操作检测到版本号不一致时会抛出 OptimisticLockingFailureException,从而防止脏写。@GeoSpatialIndexed 定义地理空间索引,用于后续的 $near、$geoWithin 查询。Map 类型的字段可以灵活存储动态属性,MongoDB 的文档模型天然支持异构数据结构。
3. Repository 接口
继承 MongoRepository 后,Spring Data MongoDB 会根据方法签名自动生成查询实现,这称为派生查询方法。命名规则遵循 findBy + 属性名 + 操作符 的模式,支持 And、Or、Like、Between、In、Exists 等多种组合。对于复杂查询,可使用 @Query 注解直接编写原生 JSON 查询,或使用 @Aggregation 注解声明聚合管道。
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByCategoryAndPriceLessThan(String category, BigDecimal price);
List<Product> findByNameContainingIgnoreCase(String name);
Page<Product> findByCategoryOrderByPriceDesc(String category, Pageable pageable);
boolean existsByName(String name);
long countByCategory(String category);
@Query("{ 'category': ?0, 'price': { $gte: ?1, $lte: ?2 } }")
List<Product> findByPriceRange(String category, BigDecimal min, BigDecimal max);
@Query(value = "{ 'category': ?0 }", fields = "{ 'name': 1, 'price': 1 }")
List<Product> findNamesAndPricesByCategory(String category);
@Aggregation(pipeline = {
"{ $match: { category: ?0 } }",
"{ $group: { _id: null, avgPrice: { $avg: '$price' }, totalStock: { $sum: '$stock' } } }"
})
ProductStats getStatsByCategory(String category);
@Aggregation(pipeline = {
"{ $match: { category: ?0 } }",
"{ $lookup: { from: 'suppliers', localField: 'supplier.$id', foreignField: '_id', as: 'supplierInfo' } }",
"{ $unwind: '$supplierInfo' }",
"{ $project: { name: 1, supplierName: '$supplierInfo.name' } }"
})
List<ProductSupplierView> getProductsWithSupplier(String category);
}
fields = "{ 'name': 1, 'price': 1 }" 属于投影操作,仅返回指定字段,减少网络传输量和内存占用。$lookup 是 MongoDB 中的左外连接操作,在聚合管道中可实现跨集合关联查询,替代部分 @DBRef 的使用场景以提升性能。
4. MongoTemplate
MongoTemplate 是 Spring Data MongoDB 的核心操作类,提供了比 Repository 更灵活的控制能力,特别适合构建动态条件查询、执行批量操作以及编写复杂聚合管道。Criteria 是构建查询条件的基石,支持链式调用组合多个条件。BulkOperations 则可以将多个写操作合并为一次网络往返,显著提升写入吞吐量。
@Service
public class ProductService {
@Autowired private MongoTemplate mongoTemplate;
public List<Product> advancedSearch(String keyword, String category,
BigDecimal minPrice, BigDecimal maxPrice,
Pageable pageable) {
Query query = new Query();
if (StringUtils.hasText(keyword)) {
query.addCriteria(Criteria.where("name").regex(keyword, "i"));
}
if (StringUtils.hasText(category)) {
query.addCriteria(Criteria.where("category").is(category));
}
if (minPrice != null && maxPrice != null) {
query.addCriteria(Criteria.where("price").gte(minPrice).lte(maxPrice));
}
query.with(Sort.by(Sort.Direction.DESC, "createdAt"));
query.with(pageable);
return mongoTemplate.find(query, Product.class);
}
public void updateStockBatch(Map<String, Integer> stockChanges) {
BulkOperations bulkOps = mongoTemplate.bulkOps(
BulkOperations.BulkMode.UNORDERED, Product.class);
stockChanges.forEach((id, qty) -> {
Query query = Query.query(Criteria.where("_id").is(id));
Update update = new Update().inc("stock", qty);
bulkOps.updateOne(query, update);
});
bulkOps.execute();
}
public List<Document> getCategoryRevenue() {
Aggregation agg = Aggregation.newAggregation(
Aggregation.match(Criteria.where("status").is("completed")),
Aggregation.unwind("items"),
Aggregation.group("items.category")
.sum(ArithmeticOperators.Multiply.valueOf("items.price")
.multiplyBy("items.quantity")).as("revenue"),
Aggregation.sort(Sort.Direction.DESC, "revenue"),
Aggregation.project("revenue").and("_id").as("category").andExclude("_id")
);
return mongoTemplate.aggregate(agg, "orders", Document.class)
.getMappedResults();
}
public void upsertProduct(Product product) {
Query query = Query.query(Criteria.where("sku").is(product.getSku()));
Update update = new Update()
.setOnInsert("createdAt", Instant.now())
.set("name", product.getName())
.set("price", product.getPrice())
.set("updatedAt", Instant.now());
mongoTemplate.upsert(query, update, Product.class);
}
}
setOnInsert 在 upsert 场景中尤为有用,仅在文档初次插入时生效,不会覆盖已有字段。对于聚合管道,建议使用 Aggregation.project 重命名字段,让输出结果更符合业务语义。BulkMode.UNORDERED 允许个别操作失败后继续执行剩余操作,适用于日志写入等容错性高的场景;ORDERED 则在出错时立即停止。
5. 事务
MongoDB 的事务自 4.0 版本引入,在单副本集上支持多文档 ACID 事务,4.2 之后扩展至分片集群。Spring Data MongoDB 通过 @Transactional 注解和 MongoTransactionManager 将 MongoDB 事务无缝集成到 Spring 的事务抽象层中。事务中的读写操作,要么全部成功提交,要么全部回滚。需要注意的是,事务必须在副本集或分片集群上运行,且会话开启后所有涉及的数据库和集合都不能为空配置。
@Service
public class OrderService {
@Autowired private MongoTemplate mongoTemplate;
@Autowired private MongoTransactionManager transactionManager;
@Transactional
public void createOrder(OrderRequest request) {
Product product = mongoTemplate.findOne(
Query.query(Criteria.where("sku").is(request.sku())),
Product.class
);
if (product == null || product.getStock() < request.qty()) {
throw new IllegalStateException("库存不足或商品不存在");
}
mongoTemplate.updateFirst(
Query.query(Criteria.where("sku").is(request.sku())),
new Update().inc("stock", -request.qty()),
Product.class
);
Order order = new Order(request);
order.setCreatedAt(Instant.now());
mongoTemplate.save(order);
mongoTemplate.save(new InventoryLog(request.sku(), -request.qty(), "出库"));
}
@Transactional
public void cancelOrder(String orderId) {
Order order = mongoTemplate.findById(orderId, Order.class);
if (order == null || order.isCancelled()) return;
mongoTemplate.updateFirst(
Query.query(Criteria.where("_id").is(orderId)),
new Update().set("cancelled", true).set("cancelledAt", Instant.now()),
Order.class
);
mongoTemplate.updateFirst(
Query.query(Criteria.where("sku").is(order.getSku())),
new Update().inc("stock", order.getQty()),
Product.class
);
}
}
在事务内部,MongoDB 会默认以快照隔离级别执行,读取操作看到的是事务开始时的数据快照,避免了幻读和不可重复读问题。当事务抛出异常时,Spring 会自动调用 session.abortTransaction() 回滚所有更改,已写入的文档和日志不会持久化到数据文件中。
6. Change Streams 实时数据变更监听
Change Streams 是 MongoDB 3.6 引入的一项特性,允许应用程序以流式方式监听集合或数据库级别的变更事件,包括 insert、update、replace、delete、invalidate 等操作。与轮询相比,Change Streams 基于 oplog 实现,具有毫秒级延迟且无额外查询开销,是实现实时数据同步、缓存失效和事件驱动架构的理想选择。
@Service
public class ChangeStreamListener implements CommandLineRunner {
@Autowired private MongoTemplate mongoTemplate;
@Override
public void run(String... args) {
ChangeStreamOptions options = ChangeStreamOptions.builder()
.filter(Aggregation.newAggregation(
Aggregation.match(Criteria.where("operationType")
.in("insert", "update", "replace"))
))
.fullDocumentLookup(FullDocument.UPDATE_LOOKUP)
.build();
MessageListenerContainer container = new DefaultMessageListenerContainer(mongoTemplate);
container.start();
ChangeStreamRequest<Document> request = ChangeStreamRequest.builder()
.collection("products")
.publishTo(doc -> {
String op = doc.getString("operationType");
Document fullDoc = doc.get("fullDocument", Document.class);
System.out.println("[ChangeStream] 操作类型: " + op + ", 文档: " + fullDoc);
})
.build();
container.register(request, Document.class);
}
}
UPDATE_LOOKUP 选项在更新事件中嵌入变更后的完整文档,避免了客户端需要二次查询才能获取最新状态的麻烦。由于 Change Streams 依赖 oplog,因此 oplog 的保留窗口需要足够长,防止应用短暂离线后无法恢复断点续传。在实际部署中,可以将监听到的变更事件转发到 Kafka、RabbitMQ 或 Redis Pub/Sub,解耦生产端和消费端。
7. GridFS 大文件分块存储
当需要存储超过 16MB 的文件时,MongoDB 的单个文档大小限制决定了无法将文件内容直接存入标准文档集合。GridFS 是 MongoDB 内置的规范,将大文件拆分为 255KB 的 chunk 分片存储,并通过 fs.files 和 fs.chunks 两个集合记录元数据和内容块。Spring Data MongoDB 通过 GridFsTemplate 提供了对 GridFS 的友好封装。
@Service
public class FileStorageService {
@Autowired private GridFsTemplate gridFsTemplate;
@Autowired private GridFsOperations gridFsOperations;
public String storeFile(InputStream inputStream, String filename,
String contentType, ObjectId userId) {
DBObject metadata = new BasicDBObject();
metadata.put("userId", userId.toString());
metadata.put("uploadDate", Instant.now().toString());
metadata.put("contentType", contentType);
ObjectId fileId = gridFsTemplate.store(
inputStream, filename, contentType, metadata);
return fileId.toString();
}
public GridFSFile findFile(String fileId) {
return gridFsTemplate.findOne(
Query.query(Criteria.where("_id").is(new ObjectId(fileId))));
}
public void downloadFile(String fileId, OutputStream out) throws IOException {
GridFSFile file = findFile(fileId);
if (file == null) return;
GridFsResource resource = gridFsOperations.getResource(file);
try (InputStream in = resource.getInputStream()) {
in.transferTo(out);
}
}
public void deleteFile(String fileId) {
gridFsTemplate.delete(
Query.query(Criteria.where("_id").is(new ObjectId(fileId))));
}
}
gridFsTemplate.store 会自动管理文件分块、写入 fs.files 和 fs.chunks,并返回文件的唯一标识符。读取时通过 GridFsOperations.getResource 将分散的 chunk 重新拼接为完整流,调用方无需关心底层分片细节。对于图片、PDF、视频等二进制大对象场景,GridFS 结合 CDN 边缘缓存可以构建高效的文件分发系统。
8. GeoSpatial 地理空间查询($near、$geoWithin)
MongoDB 支持 2dsphere 和 2d 两种地理空间索引。2dsphere 基于地球表面球面几何计算距离,适合真实世界的坐标数据;2d 则是平面坐标系,适合游戏地图等虚拟场景。Spring Data MongoDB 通过 NearQuery 和 Criteria 封装了 $near、$nearSphere、$geoWithin 等操作符,开发者只需提供 Point 或 GeoJsonPoint 对象即可完成空间查询。
@Service
public class LocationService {
@Autowired private MongoTemplate mongoTemplate;
public List<Product> findNearby(double longitude, double latitude,
double maxDistanceKm) {
Point point = new Point(longitude, latitude);
Distance distance = new Distance(maxDistanceKm, Metrics.KILOMETERS);
NearQuery nearQuery = NearQuery.near(point)
.maxDistance(distance)
.spherical(true);
GeoResults<Product> results = mongoTemplate.geoNear(
nearQuery, Product.class);
return results.getContent().stream()
.map(GeoResult::getContent)
.toList();
}
public List<Product> findWithinPolygon(List<double[]> polygon) {
GeoJsonPolygon geoPolygon = new GeoJsonPolygon(
polygon.stream().map(p -> new Point(p[0], p[1])).toList()
);
Query query = new Query(
Criteria.where("location").within(geoPolygon));
return mongoTemplate.find(query, Product.class);
}
public List<Product> findWithinCircle(double longitude, double latitude,
double radiusKm) {
Circle circle = new Circle(longitude, latitude, radiusKm / 6378.1);
Query query = new Query(
Criteria.where("location").withinSphere(circle));
return mongoTemplate.find(query, Product.class);
}
}
$geoWithin 支持多边形、圆形和框形区域查询,常用于外卖配送范围、地理围栏等业务。$near 返回的结果会按距离升序排列,并可结合 spherical(true) 使用球面距离公式,避免地图投影带来的误差。地理空间索引的构建需要确保坐标数组顺序为 [longitude, latitude],否则会导致查询异常。
9. Text Search 全文检索与权重排序
MongoDB 的全文索引通过为字段创建 text 索引来支持基于词干分析的自然语言搜索,能够匹配关键字、忽略大小写和标点,并按相关性评分排序。每个集合最多只能创建一个全文索引,但该索引可以覆盖多个字段。Spring Data MongoDB 通过 TextCriteria 和 TextQuery 提供对 $text 和 $meta 操作符的封装,支持设置语言模型、精确短语搜索以及区分大小写。
@Service
public class SearchService {
@Autowired private MongoTemplate mongoTemplate;
public List<Product> textSearch(String keywords, String language) {
TextCriteria criteria = TextCriteria.forLanguage(language)
.matching(keywords);
Query query = TextQuery.queryText(criteria)
.sortByScore()
.with(PageRequest.of(0, 20));
return mongoTemplate.find(query, Product.class);
}
public List<Product> textSearchWithFields(String keywords) {
TextCriteria criteria = TextCriteria.forDefaultLanguage()
.matching(keywords)
.caseSensitive(false)
.diacriticSensitive(false);
Query query = TextQuery.queryText(criteria)
.sortByScore()
.includeScore("score");
return mongoTemplate.find(query, Product.class);
}
}
在实体类中,可以通过 weights 参数为不同字段指定搜索权重:例如商品名称的权重设为 10,而描述的权重设为 1,这样当关键词同时匹配两者时,名称匹配的结果会排在前面。对于中文搜索,需将 language 设为 "none" 以避免英文词干分析器错误地拆分汉字。
10. 索引优化与 Explain 分析
索引是 MongoDB 查询性能的基石。Spring Data MongoDB 提供了 @Indexed、@CompoundIndex 和 @TextIndexed 等注解,在应用启动后自动创建相应索引。建议在实际开发中结合 MongoDB 的 explain 命令分析查询计划,确认查询是否命中预期索引、执行的扫描方式是全表扫描还是索引扫描、以及排序操作是否充分利用索引的有序特性。
@Document(collection = "products")
@CompoundIndex(name = "category_price", def = "{'category': 1, 'price': -1}")
public class Product {
@Id private String id;
@Indexed
private String sku;
@TextIndexed(weight = 10)
private String name;
@TextIndexed(weight = 1)
private String description;
@Indexed(expireAfterSeconds = 86400)
private Instant expireAt;
}
@Service
public class IndexService {
@Autowired private MongoTemplate mongoTemplate;
public void analyzeQuery(Query query, String collectionName) {
Document explainResult = mongoTemplate.executeCommand(
new Document("explain", new Document("find", collectionName)
.append("filter", query.getQueryObject()))
.append("verbosity", "executionStats")
);
Document queryPlanner = explainResult.get("queryPlanner", Document.class);
Document winningPlan = queryPlanner.get("winningPlan", Document.class);
String stage = winningPlan.getString("stage");
Document executionStats = explainResult.get("executionStats", Document.class);
long docsExamined = executionStats.getLong("totalDocsExamined");
long keysExamined = executionStats.getLong("totalKeysExamined");
System.out.println("查询阶段: " + stage);
System.out.println("扫描文档数: " + docsExamined);
System.out.println("扫描键值数: " + keysExamined);
}
public void createIndexManually() {
IndexOps indexOps = mongoTemplate.indexOps(Product.class);
Index index = new Index()
.on("createdAt", Sort.Direction.ASC)
.named("createdAt_asc");
indexOps.ensureIndex(index);
}
}
executionStats 是性能诊断的关键输出,totalDocsExamined 远大于返回结果数时通常意味着查询未命中索引或索引选择不当。expireAfterSeconds 可创建 TTL 索引,让 MongoDB 自动删除过期文档,非常适合会话记录、验证码缓存等有时效性的数据。复合索引的字段顺序至关重要,应将高选择性的字段放在前面,让 MongoDB 能够利用前缀匹配快速缩小扫描范围。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。