Spring Boot 整合 MongoDB:CRUD 与聚合查询实战指南
一、环境配置与基础集成
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: localhost
port: 27017
database: spring_mongo
authentication-database: admin # 认证数据库
username: admin
password: admin123
auto-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;
@PostMapping
public ResponseEntity<User> create(@RequestBody User user) {
if (userService.existsByUsername(user.getUsername())) {
throw new RuntimeException("用户名已存在");
}
return ResponseEntity.ok(userService.createUser(user));
}
@GetMapping
public 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;
@Transactional
public 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 {
@Autowired
private MongoTemplate mongoTemplate;
@PostConstruct
public 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 {
@Autowired
private UserRepository userRepository;
@Autowired
private MongoTemplate mongoTemplate;
@BeforeEach
void 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));
}
@Test
void testFindByUsername() {
Optional<User> user = userRepository.findByUsername("user1");
assertTrue(user.isPresent());
assertEquals("user1@test.com", user.get().getEmail());
}
@Test
void testFindByAddressCity() {
List<User> users = userRepository.findByAddress_City("北京");
assertEquals(1, users.size());
assertEquals("user1", users.get(0).getUsername());
}
}
5.2 集成测试示例
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserRepository userRepository;
@Test
void 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 {
@Id
private String id;
private String userId;
private ActionType actionType; // VIEW, CLICK, ADD_TO_CART, PURCHASE
private String productId;
private String categoryId;
private BigDecimal price;
private Date actionTime;
// 用户设备信息
private String deviceType; // MOBILE, DESKTOP, TABLET
private 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() {
@Override
public 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: localhost
port: 27017
database: spring_mongo
username: admin
password: admin123
auto-index-creation: true
# 连接池配置
options:
min-connections-per-host: 10
max-connections-per-host: 100
threads-allowed-to-block-for-connection-multiplier: 5
max-wait-time: 120000
connect-timeout: 10000
socket-timeout: 60000
server-selection-timeout: 30000
max-connection-idle-time: 60000
max-connection-life-time: 1800000
八、安全最佳实践
8.1 敏感数据加密
@Document(collection = "secure_users")
@Data
public class SecureUser {
@Id
private String id;
private String username;
@Encrypted
private String creditCardNumber;
@Encrypted
private String ssn;
// 其他非敏感字段
private String address;
private String phone;
}
@Configuration
public class MongoEncryptionConfig {
@Bean
public EncryptionKeyResolver encryptionKeyResolver() {
return new EncryptionKeyResolver() {
@Override
public 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 {
@CreatedBy
private String createdBy;
@CreatedDate
private Date createdDate;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate
private Date lastModifiedDate;
}
@Configuration
@EnableMongoAuditing
public class MongoAuditConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
}
}
// 实体类继承AuditableEntity
@Document(collection = "products")
public class Product extends AuditableEntity {
@Id
private String id;
private String name;
private BigDecimal price;
// 其他字段...
}
九、总结与最佳实践
9.1 核心经验总结
- 文档设计原则:
- 根据查询模式设计文档结构
- 合理使用嵌入和引用
- 控制文档大小,避免超过16MB限制
- 性能优化要点:
- 为常用查询创建适当索引
- 使用投影减少网络传输
- 批量操作替代单条操作
- 事务使用建议:
- 仅在必要时使用多文档事务
- 控制事务持续时间
- 处理乐观锁冲突
9.2 推荐架构模式
9.3 扩展阅读建议
- MongoDB官方文档:
- 聚合管道
- 事务
- 性能优化
- Spring Data MongoDB参考:
- 自定义Repository
- 查询DSL
- 审计功能