Spring Boot 整合 MongoDB:CRUD 与聚合查询实战
Spring Boot 整合 MongoDB:CRUD 与聚合查询实战指南
- 一、环境配置与基础集成
- 1.1 项目初始化与依赖配置
- 1.2 配置文件设置
- 1.3 实体类设计
- 二、基础 CRUD 操作实现
- 2.1 创建 Repository 接口
- 2.2 服务层实现
- 2.3 控制器层
- 三、高级查询与聚合操作
- 3.1 使用 MongoTemplate 实现复杂查询
- 3.2 聚合查询实战
- 3.2.1 基础聚合操作
- 3.2.2 多阶段聚合管道
- 3.2.3 联表查询 ($lookup)
- 四、事务管理与性能优化
- 4.1 MongoDB 事务支持
- 4.2 索引优化策略
- 4.3 批量操作优化
- 五、测试与验证
- 5.1 单元测试配置
- 5.2 集成测试示例
- 六、实战案例:电商用户行为分析系统
- 6.1 数据模型设计
- 6.2 核心分析功能实现
- 6.2.1 用户行为漏斗分析
- 6.2.2 热门商品分析
- 七、性能监控与调优
- 7.1 查询性能分析
- 7.2 连接池配置优化
- 八、安全最佳实践
- 8.1 敏感数据加密
- 8.2 审计功能实现
- 九、总结与最佳实践
- 9.1 核心经验总结
- 9.2 推荐架构模式
- 9.3 扩展阅读建议
一、环境配置与基础集成
1.1 项目初始化与依赖配置
首先创建一个新的 Spring Boot 项目,添加以下关键依赖到 pom.xml:
<dependencies><!-- Spring Data MongoDB --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!-- Lombok 简化代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- 测试支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>
1.2 配置文件设置
在 application.yml 中配置 MongoDB 连接:
spring:data:mongodb:host: localhostport: 27017database: spring_mongoauthentication-database: admin # 认证数据库username: adminpassword: admin123auto-index-creation: true # 自动创建索引
1.3 实体类设计
创建基础实体类和带有 MongoDB 注解的领域模型:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Document(collection = "users") // 指定集合名称
public class User {@Id // 主键标识private String id;@Indexed(unique = true) // 唯一索引private String username;@Field("email_addr") // 自定义字段名private String email;private String password;private Integer age;private List<String> roles;private Address address; // 嵌套文档private Date createdAt;@Transient // 不持久化到数据库private String tempToken;
}@Data
@Builder
public class Address {private String country;private String city;private String street;private String zipCode;
}
二、基础 CRUD 操作实现
2.1 创建 Repository 接口
public interface UserRepository extends MongoRepository<User, String> {// 方法名自动推导查询List<User> findByUsername(String username);// 使用 @Query 注解自定义查询@Query("{ 'age' : { $gt: ?0, $lt: ?1 } }")List<User> findUsersByAgeBetween(int minAge, int maxAge);// 模糊查询List<User> findByUsernameLike(String regex);// 嵌套文档查询List<User> findByAddress_City(String city);
}
2.2 服务层实现
@Service
@RequiredArgsConstructor
public class UserService {private final UserRepository userRepository;// 创建用户public User createUser(User user) {user.setCreatedAt(new Date());return userRepository.save(user);}// 批量插入public List<User> batchCreate(List<User> users) {return userRepository.saveAll(users);}// 查询所有用户public List<User> findAllUsers() {return userRepository.findAll();}// 分页查询public Page<User> findUsersByPage(int page, int size) {return userRepository.findAll(PageRequest.of(page, size, Sort.by("createdAt").descending()));}// 更新用户public User updateUser(String id, User user) {user.setId(id);return userRepository.save(user);}// 部分更新public void partialUpdate(String id, String key, Object value) {Query query = new Query(Criteria.where("id").is(id));Update update = new Update().set(key, value);mongoTemplate.updateFirst(query, update, User.class);}// 删除用户public void deleteUser(String id) {userRepository.deleteById(id);}// 检查用户名是否存在public boolean existsByUsername(String username) {return userRepository.existsByUsername(username);}
}
2.3 控制器层
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {private final UserService userService;@PostMappingpublic ResponseEntity<User> create(@RequestBody User user) {if (userService.existsByUsername(user.getUsername())) {throw new RuntimeException("用户名已存在");}return ResponseEntity.ok(userService.createUser(user));}@GetMappingpublic ResponseEntity<List<User>> listAll() {return ResponseEntity.ok(userService.findAllUsers());}@GetMapping("/page")public ResponseEntity<Page<User>> listByPage(@RequestParam(defaultValue = "0") int page,@RequestParam(defaultValue = "10") int size) {return ResponseEntity.ok(userService.findUsersByPage(page, size));}@PutMapping("/{id}")public ResponseEntity<User> update(@PathVariable String id, @RequestBody User user) {return ResponseEntity.ok(userService.updateUser(id, user));}@DeleteMapping("/{id}")public ResponseEntity<Void> delete(@PathVariable String id) {userService.deleteUser(id);return ResponseEntity.noContent().build();}
}
三、高级查询与聚合操作
3.1 使用 MongoTemplate 实现复杂查询
@Service
@RequiredArgsConstructor
public class UserQueryService {private final MongoTemplate mongoTemplate;// 多条件动态查询public List<User> complexQuery(String username, Integer minAge, String city) {Criteria criteria = new Criteria();if (StringUtils.hasText(username)) {criteria.and("username").regex(username, "i");}if (minAge != null) {criteria.and("age").gte(minAge);}if (StringUtils.hasText(city)) {criteria.and("address.city").is(city);}Query query = new Query(criteria);return mongoTemplate.find(query, User.class);}// 字段投影public List<User> findUsersWithSelectedFields() {Query query = new Query();query.fields().include("username").include("email").include("address.city");return mongoTemplate.find(query, User.class);}// 排序与限制public List<User> findTop5OldestUsers() {Query query = new Query().with(Sort.by(Sort.Direction.DESC, "age")).limit(5);return mongoTemplate.find(query, User.class);}
}
3.2 聚合查询实战
3.2.1 基础聚合操作
public List<AgeGroup> groupUsersByAge() {Aggregation aggregation = Aggregation.newAggregation(Aggregation.group("age").count().as("count").addToSet("username").as("usernames"),Aggregation.sort(Sort.Direction.DESC, "count"));return mongoTemplate.aggregate(aggregation, "users", AgeGroup.class).getMappedResults();
}@Data
public static class AgeGroup {private Integer age;private Integer count;private List<String> usernames;
}
3.2.2 多阶段聚合管道
public List<CityStats> getCityStatistics() {Aggregation aggregation = Aggregation.newAggregation(// 按城市分组Aggregation.group("address.city").count().as("userCount").avg("age").as("averageAge").max("age").as("maxAge").min("age").as("minAge"),// 添加计算字段Aggregation.project().and("city").previousOperation().and("userCount").as("userCount").and("averageAge").as("averageAge").and("maxAge").as("maxAge").and("minAge").as("minAge").andExpression("userCount / [0]", totalUserCount()).as("percentage"),// 排序Aggregation.sort(Sort.Direction.DESC, "userCount"),// 限制结果Aggregation.limit(10));return mongoTemplate.aggregate(aggregation, "users", CityStats.class).getMappedResults();
}private long totalUserCount() {return mongoTemplate.count(new Query(), User.class);
}@Data
public static class CityStats {private String city;private Long userCount;private Double averageAge;private Integer maxAge;private Integer minAge;private Double percentage;
}
3.2.3 联表查询 ($lookup)
public List<UserWithOrders> getUsersWithOrders() {Aggregation aggregation = Aggregation.newAggregation(Aggregation.lookup("orders", "id", "userId", "orders"),Aggregation.project().and("id").as("userId").and("username").as("username").and("email").as("email").and("orders").as("orders").andExclude("_id"));return mongoTemplate.aggregate(aggregation, "users", UserWithOrders.class).getMappedResults();
}@Data
public static class UserWithOrders {private String userId;private String username;private String email;private List<Order> orders;
}@Data
public static class Order {private String orderId;private BigDecimal amount;private Date orderDate;
}
四、事务管理与性能优化
4.1 MongoDB 事务支持
@Service
@RequiredArgsConstructor
public class TransactionalService {private final MongoTemplate mongoTemplate;private final UserRepository userRepository;@Transactionalpublic void transferPoints(String fromUserId, String toUserId, int points) {// 检查用户是否存在User fromUser = userRepository.findById(fromUserId).orElseThrow(() -> new RuntimeException("转出用户不存在"));User toUser = userRepository.findById(toUserId).orElseThrow(() -> new RuntimeException("转入用户不存在"));// 检查余额if (fromUser.getPoints() < points) {throw new RuntimeException("积分不足");}// 更新转出用户Query fromQuery = new Query(Criteria.where("id").is(fromUserId));Update fromUpdate = new Update().inc("points", -points);mongoTemplate.updateFirst(fromQuery, fromUpdate, User.class);// 更新转入用户Query toQuery = new Query(Criteria.where("id").is(toUserId));Update toUpdate = new Update().inc("points", points);mongoTemplate.updateFirst(toQuery, toUpdate, User.class);// 记录交易日志TransactionLog log = TransactionLog.builder().fromUserId(fromUserId).toUserId(toUserId).points(points).createdAt(new Date()).build();mongoTemplate.insert(log);}
}
4.2 索引优化策略
@Configuration
public class MongoIndexConfig {@Autowiredprivate MongoTemplate mongoTemplate;@PostConstructpublic void initIndexes() {// 复合索引IndexOperations userIndexOps = mongoTemplate.indexOps(User.class);IndexDefinition compoundIndex = new Index().on("username", Sort.Direction.ASC).on("email", Sort.Direction.ASC).named("username_email_compound_index");userIndexOps.ensureIndex(compoundIndex);// TTL索引 (自动过期)IndexDefinition ttlIndex = new Index().on("createdAt", Sort.Direction.ASC).expire(30, TimeUnit.DAYS);userIndexOps.ensureIndex(ttlIndex);// 文本索引IndexDefinition textIndex = new Index().on("username", Sort.Direction.ASC).on("email", Sort.Direction.ASC).named("user_text_search").text();userIndexOps.ensureIndex(textIndex);}
}
4.3 批量操作优化
public void bulkInsertUsers(List<User> users) {BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.ORDERED, User.class);for (User user : users) {bulkOps.insert(user);}bulkOps.execute();
}public void bulkUpdateUserPoints(Map<String, Integer> userIdToPointsMap) {BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, User.class);userIdToPointsMap.forEach((userId, points) -> {Query query = new Query(Criteria.where("id").is(userId));Update update = new Update().inc("points", points);bulkOps.updateOne(query, update);});BulkWriteResult result = bulkOps.execute();log.info("Updated {} documents", result.getModifiedCount());
}
五、测试与验证
5.1 单元测试配置
@DataMongoTest
@ExtendWith(SpringExtension.class)
public class UserRepositoryTest {@Autowiredprivate UserRepository userRepository;@Autowiredprivate MongoTemplate mongoTemplate;@BeforeEachvoid setup() {// 清空集合mongoTemplate.dropCollection(User.class);// 初始化测试数据User user1 = User.builder().username("user1").email("user1@test.com").age(25).address(Address.builder().city("北京").country("中国").build()).build();User user2 = User.builder().username("user2").email("user2@test.com").age(30).address(Address.builder().city("上海").country("中国").build()).build();userRepository.saveAll(List.of(user1, user2));}@Testvoid testFindByUsername() {Optional<User> user = userRepository.findByUsername("user1");assertTrue(user.isPresent());assertEquals("user1@test.com", user.get().getEmail());}@Testvoid testFindByAddressCity() {List<User> users = userRepository.findByAddress_City("北京");assertEquals(1, users.size());assertEquals("user1", users.get(0).getUsername());}
}
5.2 集成测试示例
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {@Autowiredprivate MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate UserRepository userRepository;@Testvoid testCreateAndGetUser() throws Exception {User newUser = User.builder().username("testuser").email("test@example.com").age(28).build();// 创建用户mockMvc.perform(post("/api/users").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(newUser))).andExpect(status().isOk()).andExpect(jsonPath("$.username").value("testuser"));// 查询用户mockMvc.perform(get("/api/users")).andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(1))));}
}
六、实战案例:电商用户行为分析系统
6.1 数据模型设计
@Data
@Document(collection = "user_actions")
public class UserAction {@Idprivate String id;private String userId;private ActionType actionType; // VIEW, CLICK, ADD_TO_CART, PURCHASEprivate String productId;private String categoryId;private BigDecimal price;private Date actionTime;// 用户设备信息private String deviceType; // MOBILE, DESKTOP, TABLETprivate String os;private String browser;// 地理位置信息private String country;private String city;private String ipAddress;
}public enum ActionType {VIEW,CLICK,ADD_TO_CART,REMOVE_FROM_CART,PURCHASE,SEARCH,LOGIN,LOGOUT
}
6.2 核心分析功能实现
6.2.1 用户行为漏斗分析
public FunnelAnalysisResult analyzeUserFunnel(Date startDate, Date endDate) {Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(Criteria.where("actionTime").gte(startDate).lte(endDate)),Aggregation.group("userId").push("actionType").as("actions"),Aggregation.project().and("_id").as("userId").and("actions").as("actions").and(ArrayOperators.Size.lengthOfArray("actions")).as("actionCount"),Aggregation.match(Criteria.where("actionCount").gte(3)),Aggregation.facet().and(Aggregation.match(Criteria.where("actions").all("VIEW", "ADD_TO_CART", "PURCHASE")),Aggregation.count().as("completeFunnelCount")).as("completeFunnel").and(Aggregation.match(Criteria.where("actions").all("VIEW", "ADD_TO_CART")),Aggregation.count().as("cartAbandonCount")).as("cartAbandon").and(Aggregation.match(Criteria.where("actions").is("VIEW")),Aggregation.count().as("viewOnlyCount")).as("viewOnly"),Aggregation.project().and("completeFunnel.completeFunnelCount").as("completeFunnelCount").and("cartAbandon.cartAbandonCount").as("cartAbandonCount").and("viewOnly.viewOnlyCount").as("viewOnlyCount").andExpression("completeFunnelCount / viewOnlyCount * 100").as("conversionRate"));return mongoTemplate.aggregate(aggregation, "user_actions", FunnelAnalysisResult.class).getUniqueMappedResult();
}@Data
public static class FunnelAnalysisResult {private int completeFunnelCount;private int cartAbandonCount;private int viewOnlyCount;private double conversionRate;
}
6.2.2 热门商品分析
public List<ProductAnalysis> analyzeTopProducts(int limit, Date startDate, Date endDate) {Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(Criteria.where("actionTime").gte(startDate).lte(endDate).and("actionType").in("VIEW", "ADD_TO_CART", "PURCHASE")),Aggregation.group("productId").count().as("viewCount").sum(ConditionalOperators.when(Criteria.where("actionType").is("ADD_TO_CART")).then(1).otherwise(0)).as("cartAddCount").sum(ConditionalOperators.when(Criteria.where("actionType").is("PURCHASE")).then(1).otherwise(0)).as("purchaseCount").avg("price").as("avgPrice"),Aggregation.project().and("_id").as("productId").and("viewCount").as("viewCount").and("cartAddCount").as("cartAddCount").and("purchaseCount").as("purchaseCount").and("avgPrice").as("avgPrice").andExpression("purchaseCount / viewCount * 100").as("conversionRate"),Aggregation.sort(Sort.Direction.DESC, "purchaseCount"),Aggregation.limit(limit));return mongoTemplate.aggregate(aggregation, "user_actions", ProductAnalysis.class).getMappedResults();
}@Data
public static class ProductAnalysis {private String productId;private long viewCount;private long cartAddCount;private long purchaseCount;private double avgPrice;private double conversionRate;
}
七、性能监控与调优
7.1 查询性能分析
public void analyzeQueryPerformance() {// 启用查询分析mongoTemplate.setQueryMetaDataProvider(new QueryMetaDataProvider() {@Overridepublic Document getMetaData(MongoAction mongoAction) {return new Document("comment", "performance analysis");}});// 执行查询并获取分析结果Query query = new Query(Criteria.where("age").gt(25));query.withHint("age_1"); // 强制使用特定索引List<User> users = mongoTemplate.find(query, User.class);// 获取查询执行统计MongoDatabase db = mongoTemplate.getDb();Document profileResult = db.runCommand(new Document("profile", 2)); // 2=全量分析log.info("Query profile results: {}", profileResult.toJson());
}
7.2 连接池配置优化
spring:data:mongodb:host: localhostport: 27017database: spring_mongousername: adminpassword: admin123auto-index-creation: true# 连接池配置options:min-connections-per-host: 10max-connections-per-host: 100threads-allowed-to-block-for-connection-multiplier: 5max-wait-time: 120000connect-timeout: 10000socket-timeout: 60000server-selection-timeout: 30000max-connection-idle-time: 60000max-connection-life-time: 1800000
八、安全最佳实践
8.1 敏感数据加密
@Document(collection = "secure_users")
@Data
public class SecureUser {@Idprivate String id;private String username;@Encryptedprivate String creditCardNumber;@Encryptedprivate String ssn;// 其他非敏感字段private String address;private String phone;
}@Configuration
public class MongoEncryptionConfig {@Beanpublic EncryptionKeyResolver encryptionKeyResolver() {return new EncryptionKeyResolver() {@Overridepublic Map<String, byte[]> getEncryptionKeys() {// 从安全存储获取加密密钥Map<String, byte[]> keys = new HashMap<>();keys.put("creditCardKey", loadKeyFromVault("creditCardKey"));keys.put("ssnKey", loadKeyFromVault("ssnKey"));return keys;}};}private byte[] loadKeyFromVault(String keyId) {// 实现从安全存储获取密钥的逻辑return new byte[32]; // 示例返回32字节密钥}
}
8.2 审计功能实现
@Document
public abstract class AuditableEntity {@CreatedByprivate String createdBy;@CreatedDateprivate Date createdDate;@LastModifiedByprivate String lastModifiedBy;@LastModifiedDateprivate Date lastModifiedDate;
}@Configuration
@EnableMongoAuditing
public class MongoAuditConfig {@Beanpublic AuditorAware<String> auditorAware() {return () -> Optional.ofNullable(SecurityContextHolder.getContext()).map(SecurityContext::getAuthentication).map(Authentication::getName);}
}// 实体类继承AuditableEntity
@Document(collection = "products")
public class Product extends AuditableEntity {@Idprivate String id;private String name;private BigDecimal price;// 其他字段...
}
九、总结与最佳实践
9.1 核心经验总结
- 文档设计原则:
- 根据查询模式设计文档结构
- 合理使用嵌入和引用
- 控制文档大小,避免超过16MB限制
- 性能优化要点:
- 为常用查询创建适当索引
- 使用投影减少网络传输
- 批量操作替代单条操作
- 事务使用建议:
- 仅在必要时使用多文档事务
- 控制事务持续时间
- 处理乐观锁冲突
9.2 推荐架构模式
9.3 扩展阅读建议
- MongoDB官方文档:
- 聚合管道
- 事务
- 性能优化
- Spring Data MongoDB参考:
- 自定义Repository
- 查询DSL
- 审计功能