Spring Boot 整合 MongoDB:CRUD 与聚合查询实战

发布于:2025-08-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、环境配置与基础集成

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 核心经验总结

  1. 文档设计原则:
    • 根据查询模式设计文档结构
    • 合理使用嵌入和引用
    • 控制文档大小,避免超过16MB限制
  2. 性能优化要点:
    • 为常用查询创建适当索引
    • 使用投影减少网络传输
    • 批量操作替代单条操作
  3. 事务使用建议:
    • 仅在必要时使用多文档事务
    • 控制事务持续时间
    • 处理乐观锁冲突

9.2 推荐架构模式

客户端
API网关
用户服务
订单服务
商品服务
MongoDB用户库
MongoDB订单库
MongoDB商品库

9.3 扩展阅读建议

  1. MongoDB官方文档:
    • 聚合管道
    • 事务
    • 性能优化
  2. Spring Data MongoDB参考:
    • 自定义Repository
    • 查询DSL
    • 审计功能