Spring Boot 集成 MongoDB 使用文档
一、环境准备与项目初始化
1.1 创建 Spring Boot 项目
使用 Spring Initializr 创建项目,选择以下依赖:
- Spring Web
- Spring Data MongoDB
- Lombok (可选但推荐)
- Validation
或使用 Maven 依赖配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.14</version><relativePath/></parent><groupId>com.example</groupId><artifactId>mongodb-demo</artifactId><version>1.0.0</version><name>mongodb-demo</name><properties><java.version>11</java.version><mongo.driver.version>4.7.2</mongo.driver.version></properties><dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Data MongoDB --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!-- MongoDB Reactive Support (可选) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb-reactive</artifactId></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- Validation --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- DevTools --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
1.2 配置文件
application.yml 配置:
server:port: 8080servlet:context-path: /apispring:application:name: mongodb-demo# MongoDB 配置data:mongodb:# 单机配置host: localhostport: 27017database: mydatabaseusername: ${MONGO_USERNAME:admin}password: ${MONGO_PASSWORD:admin123}authentication-database: admin# 连接池配置auto-index-creation: true# 副本集配置(可选)# replica-set: rs0# uri: mongodb://user:pass@host1:27017,host2:27017,host3:27017/database?replicaSet=rs0# SSL 配置(可选)# ssl:# enabled: false# invalid-hostname-allowed: false# 自定义配置
mongodb:connection:max-pool-size: 100min-pool-size: 10max-wait-time: 120000connect-timeout: 10000socket-timeout: 0 # 0表示永不超时
application.properties 配置:
# MongoDB 配置
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase
spring.data.mongodb.username=admin
spring.data.mongodb.password=admin123
spring.data.mongodb.authentication-database=admin# 连接选项
spring.data.mongodb.auto-index-creation=true# 连接池配置
spring.data.mongodb.uri=mongodb://admin:admin123@localhost:27017/mydatabase?authSource=admin
二、实体类设计
2.1 基本实体类
package com.example.mongodbdemo.entity;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;/*** 用户实体类* @Document - 指定MongoDB集合名称* @CompoundIndex - 复合索引*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "users") // 指定集合名称,默认为类名小写
@CompoundIndexes({@CompoundIndex(name = "email_status_idx", def = "{'email': 1, 'status': 1}"),@CompoundIndex(name = "name_city_idx", def = "{'lastName': 1, 'city': 1}")
})
public class User {@Id // 主键标识private String id;@NotBlank(message = "First name is required")@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")@Field("first_name") // 指定字段名,默认使用驼峰转下划线private String firstName;@NotBlank(message = "Last name is required")@Size(min = 2, max = 50)@Field("last_name")private String lastName;@NotBlank(message = "Email is required")@Email(message = "Invalid email format")@Indexed(unique = true, background = true) // 唯一索引,后台创建private String email;@NotNull(message = "Age is required")private Integer age;@Field("phone_number")private String phoneNumber;private String gender;@Field("birth_date")private LocalDateTime birthDate;@Field("registration_date")private LocalDateTime registrationDate;private String city;private String country;@Indexed // 单字段索引private String status; // ACTIVE, INACTIVE, DELETEDprivate List<String> roles; // 数组类型@Field("preferences")private Preferences preferences; // 嵌套文档@Field("addresses")private List<Address> addresses; // 嵌套文档数组// 审计字段@Field("created_at")private LocalDateTime createdAt;@Field("updated_at")private LocalDateTime updatedAt;@Field("created_by")private String createdBy;@Field("updated_by")private String updatedBy;// 版本控制(乐观锁)@Versionprivate Long version;/*** 内嵌文档:用户偏好设置*/@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Preferences {@Field("theme")private String theme; // light, dark@Field("language")private String language;@Field("notification_enabled")private Boolean notificationEnabled;@Field("email_notifications")private Boolean emailNotifications;@Field("sms_notifications")private Boolean smsNotifications;}/*** 内嵌文档:地址信息*/@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic static class Address {@Field("type")private String type; // HOME, WORK@Field("street")private String street;@Field("city")private String city;@Field("state")private String state;@Field("postal_code")private String postalCode;@Field("country")private String country;@Field("is_default")private Boolean isDefault;}
}
2.2 产品实体类(展示更多MongoDB特性)
package com.example.mongodbdemo.entity;import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/*** 产品实体类(展示继承、多态)*/
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@Document(collection = "products")
public class Product extends BaseEntity {@NotBlank(message = "Product name is required")@Field("product_name")private String productName;@Field("sku")@NotBlank(message = "SKU is required")private String sku;@Field("description")private String description;@NotNull(message = "Price is required")@DecimalMin(value = "0.0", inclusive = false, message = "Price must be greater than 0")@Field("price")private BigDecimal price;@Field("cost_price")private BigDecimal costPrice;@Field("quantity")private Integer quantity;@Field("category")private String category;@Field("tags")private List<String> tags;@Field("attributes")private Map<String, Object> attributes; // 动态字段@Field("specifications")private List<Specification> specifications;@Field("variants")private List<ProductVariant> variants;@Field("rating")private Double rating;@Field("review_count")private Integer reviewCount;@Field("images")private List<Image> images;@Field("is_active")private Boolean isActive;@Field("is_featured")private Boolean isFeatured;@Field("meta_data")private MetaData metaData;/*** 规格*/@Data@SuperBuilderpublic static class Specification {@Field("key")private String key;@Field("value")private String value;@Field("unit")private String unit;}/*** 产品变体*/@Data@SuperBuilderpublic static class ProductVariant {@Field("variant_id")private String variantId;@Field("variant_name")private String variantName;@Field("price")private BigDecimal price;@Field("sku")private String sku;@Field("quantity")private Integer quantity;@Field("attributes")private Map<String, String> attributes;}/*** 图片*/@Data@SuperBuilderpublic static class Image {@Field("url")private String url;@Field("alt_text")private String altText;@Field("is_primary")private Boolean isPrimary;@Field("order")private Integer order;}/*** 元数据*/@Data@SuperBuilderpublic static class MetaData {@Field("seo_title")private String seoTitle;@Field("seo_description")private String seoDescription;@Field("keywords")private List<String> keywords;@Field("og_image")private String ogImage;}
}/*** 基础实体类(抽象类)*/
@Data
@SuperBuilder
abstract class BaseEntity {@org.springframework.data.annotation.Idprivate String id;@Field("created_at")private java.time.LocalDateTime createdAt;@Field("updated_at")private java.time.LocalDateTime updatedAt;@Field("created_by")private String createdBy;@Field("updated_by")private String updatedBy;@Field("is_deleted")private Boolean isDeleted;
}
三、Repository 层
3.1 基础 Repository
package com.example.mongodbdemo.repository;import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;/*** 用户Repository* 继承 MongoRepository 获得基本的CRUD操作*/
@Repository
public interface UserRepository extends MongoRepository<User, String> {// ========== 查询方法(根据方法名自动生成查询) ==========// 等值查询Optional<User> findByEmail(String email);List<User> findByFirstName(String firstName);List<User> findByLastName(String lastName);// 条件查询List<User> findByAgeGreaterThan(Integer age);List<User> findByAgeLessThan(Integer age);List<User> findByAgeBetween(Integer start, Integer end);List<User> findByAgeIn(List<Integer> ages);// 多条件查询List<User> findByFirstNameAndLastName(String firstName, String lastName);List<User> findByFirstNameOrLastName(String firstName, String lastName);// 模糊查询List<User> findByFirstNameLike(String pattern);List<User> findByFirstNameContaining(String keyword);List<User> findByFirstNameStartingWith(String prefix);List<User> findByFirstNameEndingWith(String suffix);// 忽略大小写List<User> findByFirstNameIgnoreCase(String firstName);// 嵌套文档查询List<User> findByPreferencesTheme(String theme);List<User> findByPreferencesNotificationEnabledTrue();// 数组查询List<User> findByRolesContaining(String role);// 排序和分页List<User> findByCityOrderByAgeDesc(String city);Page<User> findByStatus(String status, Pageable pageable);// 计数Long countByCity(String city);Long countByStatus(String status);// 删除void deleteByEmail(String email);Long deleteByStatus(String status);// 是否存在Boolean existsByEmail(String email);// ========== 自定义查询(使用 @Query 注解) ==========/*** 使用原生MongoDB查询语法* ?0, ?1 表示方法参数位置*/@Query("{ 'age' : { $gt: ?0, $lt: ?1 } }")List<User> findUsersByAgeRange(Integer minAge, Integer maxAge);/*** 使用 SpEL 表达式*/@Query("{ 'email' : :#{#email} }")Optional<User> findUserByEmail(@Param("email") String email);/*** 复杂查询:多条件 + 排序 + 字段投影*/@Query(value = "{ 'city': ?0, 'status': 'ACTIVE', 'age': { $gte: ?1 } }", sort = "{ 'registration_date': -1 }",fields = "{ 'firstName': 1, 'lastName': 1, 'email': 1, 'age': 1 }")List<User> findActiveUsersInCity(String city, Integer minAge);/*** 正则表达式查询*/@Query("{ 'email': { $regex: ?0, $options: 'i' } }")List<User> findUsersByEmailPattern(String pattern);/*** 数组查询*/@Query("{ 'roles': { $all: ?0 } }")List<User> findUsersWithAllRoles(List<String> roles);/*** 数组大小查询*/@Query("{ 'roles': { $size: ?0 } }")List<User> findUsersWithRoleCount(Integer count);/*** 日期范围查询*/@Query("{ 'registration_date': { $gte: ?0, $lte: ?1 } }")List<User> findUsersRegisteredBetween(LocalDateTime start, LocalDateTime end);/*** 嵌套文档查询*/@Query("{ 'addresses.city': ?0, 'addresses.is_default': true }")List<User> findUsersWithDefaultAddressInCity(String city);/*** 聚合查询:分组统计*/@Query(value = "{}", fields = "{ 'city': 1 }")List<User> findAllCities();// ========== 自定义查询方法(使用 @Aggregation 注解) ==========/*** 聚合查询:按城市分组统计用户数*/@Aggregation(pipeline = {"{ $match: { status: 'ACTIVE' } }","{ $group: { _id: '$city', count: { $sum: 1 }, avgAge: { $avg: '$age' } } }","{ $sort: { count: -1 } }","{ $limit: 10 }"})List<CityStats> countUsersByCity();/*** 聚合结果映射类*/interface CityStats {String get_id(); // MongoDB分组后的_id字段Long getCount();Double getAvgAge();}/*** 聚合查询:用户年龄分布*/@Aggregation(pipeline = {"{ $bucket: { " +"groupBy: '$age', " +"boundaries: [18, 25, 35, 45, 55, 65], " +"default: '65+', " +"output: { " +"count: { $sum: 1 }, " +"users: { $push: { name: { $concat: ['$firstName', ' ', '$lastName'] }, email: '$email' } } " +"}" +"}}"})List<AgeDistribution> getUserAgeDistribution();interface AgeDistribution {String get_id();Long getCount();List<UserInfo> getUsers();interface UserInfo {String getName();String getEmail();}}
}
3.2 自定义 Repository 实现
package com.example.mongodbdemo.repository.impl;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;/*** 自定义Repository实现*/
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserCustomRepository {private final MongoTemplate mongoTemplate;@Overridepublic Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {Query query = new Query();// 动态构建查询条件if (criteria.containsKey("firstName")) {query.addCriteria(Criteria.where("first_name").regex(criteria.get("firstName").toString(), "i"));}if (criteria.containsKey("lastName")) {query.addCriteria(Criteria.where("last_name").regex(criteria.get("lastName").toString(), "i"));}if (criteria.containsKey("email")) {query.addCriteria(Criteria.where("email").regex(criteria.get("email").toString(), "i"));}if (criteria.containsKey("ageFrom") && criteria.containsKey("ageTo")) {query.addCriteria(Criteria.where("age").gte(Integer.parseInt(criteria.get("ageFrom").toString())).lte(Integer.parseInt(criteria.get("ageTo").toString())));}if (criteria.containsKey("city")) {query.addCriteria(Criteria.where("city").is(criteria.get("city")));}if (criteria.containsKey("status")) {query.addCriteria(Criteria.where("status").is(criteria.get("status")));}// 分页和排序query.with(pageable);// 获取总数long total = mongoTemplate.count(query, User.class);// 获取数据List<User> users = mongoTemplate.find(query, User.class);return new PageImpl<>(users, pageable, total);}@Overridepublic void updateUserStatus(String userId, String status) {Query query = new Query(Criteria.where("id").is(userId));Update update = new Update().set("status", status).set("updated_at", LocalDateTime.now());mongoTemplate.updateFirst(query, update, User.class);}@Overridepublic void updateUserEmail(String userId, String newEmail) {Query query = new Query(Criteria.where("id").is(userId));Update update = new Update().set("email", newEmail).set("updated_at", LocalDateTime.now());mongoTemplate.updateFirst(query, update, User.class);}@Overridepublic void bulkUpdateUserStatus(List<String> userIds, String status) {Query query = new Query(Criteria.where("id").in(userIds));Update update = new Update().set("status", status).set("updated_at", LocalDateTime.now());mongoTemplate.updateMulti(query, update, User.class);}@Overridepublic List<Map> getUserStatistics() {// 使用聚合框架进行复杂统计Aggregation aggregation = Aggregation.newAggregation(Aggregation.match(Criteria.where("status").is("ACTIVE")),Aggregation.group("city").count().as("userCount").avg("age").as("avgAge").sum("age").as("totalAge"),Aggregation.project("userCount", "avgAge", "totalAge").and("_id").as("city"),Aggregation.sort(Sort.Direction.DESC, "userCount"));AggregationResults<Map> results = mongoTemplate.aggregate(aggregation, "users", Map.class);return results.getMappedResults();}@Overridepublic List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city) {Criteria criteria = new Criteria();List<Criteria> criteriaList = new java.util.ArrayList<>();if (firstNamePattern != null && !firstNamePattern.isEmpty()) {criteriaList.add(Criteria.where("first_name").regex(firstNamePattern, "i"));}if (minAge != null) {criteriaList.add(Criteria.where("age").gte(minAge));}if (city != null && !city.isEmpty()) {criteriaList.add(Criteria.where("city").is(city));}if (!criteriaList.isEmpty()) {criteria.andOperator(criteriaList.toArray(new Criteria[0]));}Query query = new Query(criteria);query.with(Sort.by(Sort.Direction.DESC, "registration_date"));return mongoTemplate.find(query, User.class);}@Overridepublic List<String> findAllDistinctCities() {return mongoTemplate.findDistinct(new Query(), "city", User.class, String.class);}@Overridepublic Long countUsersByCriteria(Map<String, Object> criteria) {Query query = new Query();criteria.forEach((key, value) -> {if (value != null) {switch (key) {case "ageFrom":query.addCriteria(Criteria.where("age").gte(value));break;case "ageTo":query.addCriteria(Criteria.where("age").lte(value));break;case "city":query.addCriteria(Criteria.where("city").is(value));break;case "status":query.addCriteria(Criteria.where("status").is(value));break;}}});return mongoTemplate.count(query, User.class);}
}/*** 自定义Repository接口*/
package com.example.mongodbdemo.repository.custom;import com.example.mongodbdemo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;import java.util.List;
import java.util.Map;public interface UserCustomRepository {Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable);void updateUserStatus(String userId, String status);void updateUserEmail(String userId, String newEmail);void bulkUpdateUserStatus(List<String> userIds, String status);List<Map> getUserStatistics();List<User> findUsersByCustomQuery(String firstNamePattern, Integer minAge, String city);List<String> findAllDistinctCities();Long countUsersByCriteria(Map<String, Object> criteria);
}
3.3 扩展 Repository 接口
package com.example.mongodbdemo.repository;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.custom.UserCustomRepository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;@Repository
public interface UserRepository extends MongoRepository<User, String>, UserCustomRepository {// 继承自定义接口
}
四、Service 层
4.1 基础 Service
package com.example.mongodbdemo.service;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;/*** 用户服务层*/
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {private final UserRepository userRepository;/*** 创建用户*/@Transactionalpublic User createUser(User user) {// 验证邮箱是否已存在if (userRepository.existsByEmail(user.getEmail())) {throw new RuntimeException("Email already exists: " + user.getEmail());}// 设置审计字段user.setId(null); // 确保ID由MongoDB生成user.setCreatedAt(LocalDateTime.now());user.setUpdatedAt(LocalDateTime.now());user.setStatus("ACTIVE");// 保存用户User savedUser = userRepository.save(user);log.info("Created user with ID: {}", savedUser.getId());return savedUser;}/*** 批量创建用户*/@Transactionalpublic List<User> createUsers(List<User> users) {// 验证邮箱唯一性List<String> emails = users.stream().map(User::getEmail).toList();List<User> existingUsers = userRepository.findByEmailIn(emails);if (!existingUsers.isEmpty()) {throw new RuntimeException("Some emails already exist");}// 设置审计字段LocalDateTime now = LocalDateTime.now();users.forEach(user -> {user.setId(null);user.setCreatedAt(now);user.setUpdatedAt(now);user.setStatus("ACTIVE");});return userRepository.saveAll(users);}/*** 根据ID获取用户*/public Optional<User> getUserById(String id) {return userRepository.findById(id);}/*** 根据邮箱获取用户*/public Optional<User> getUserByEmail(String email) {return userRepository.findByEmail(email);}/*** 获取所有用户*/public List<User> getAllUsers() {return userRepository.findAll();}/*** 分页获取用户*/public Page<User> getUsers(Pageable pageable) {return userRepository.findAll(pageable);}/*** 更新用户*/@Transactionalpublic User updateUser(String id, User userUpdates) {User existingUser = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found with id: " + id));// 更新允许修改的字段if (StringUtils.hasText(userUpdates.getFirstName())) {existingUser.setFirstName(userUpdates.getFirstName());}if (StringUtils.hasText(userUpdates.getLastName())) {existingUser.setLastName(userUpdates.getLastName());}if (userUpdates.getAge() != null) {existingUser.setAge(userUpdates.getAge());}if (StringUtils.hasText(userUpdates.getPhoneNumber())) {existingUser.setPhoneNumber(userUpdates.getPhoneNumber());}if (StringUtils.hasText(userUpdates.getCity())) {existingUser.setCity(userUpdates.getCity());}if (StringUtils.hasText(userUpdates.getCountry())) {existingUser.setCountry(userUpdates.getCountry());}// 更新审计字段existingUser.setUpdatedAt(LocalDateTime.now());return userRepository.save(existingUser);}/*** 部分更新用户*/@Transactionalpublic void partialUpdateUser(String id, Map<String, Object> updates) {User existingUser = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found with id: " + id));updates.forEach((key, value) -> {switch (key) {case "firstName":existingUser.setFirstName((String) value);break;case "lastName":existingUser.setLastName((String) value);break;case "age":existingUser.setAge((Integer) value);break;case "phoneNumber":existingUser.setPhoneNumber((String) value);break;case "city":existingUser.setCity((String) value);break;case "country":existingUser.setCountry((String) value);break;case "status":existingUser.setStatus((String) value);break;}});existingUser.setUpdatedAt(LocalDateTime.now());userRepository.save(existingUser);}/*** 删除用户*/@Transactionalpublic void deleteUser(String id) {if (!userRepository.existsById(id)) {throw new RuntimeException("User not found with id: " + id);}userRepository.deleteById(id);log.info("Deleted user with ID: {}", id);}/*** 软删除用户*/@Transactionalpublic void softDeleteUser(String id) {userRepository.updateUserStatus(id, "DELETED");}/*** 搜索用户*/public Page<User> searchUsers(Map<String, Object> criteria, Pageable pageable) {return userRepository.searchUsers(criteria, pageable);}/*** 根据条件查询用户*/public List<User> findUsersByCriteria(String firstNamePattern, Integer minAge, String city) {return userRepository.findUsersByCustomQuery(firstNamePattern, minAge, city);}/*** 获取用户统计*/public List<Map> getUserStatistics() {return userRepository.getUserStatistics();}/*** 获取所有城市*/public List<String> getAllCities() {return userRepository.findAllDistinctCities();}/*** 更新用户状态*/@Transactionalpublic void updateUserStatus(String id, String status) {if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {throw new RuntimeException("Invalid status: " + status);}userRepository.updateUserStatus(id, status);}/*** 批量更新用户状态*/@Transactionalpublic void bulkUpdateUserStatus(List<String> ids, String status) {if (!List.of("ACTIVE", "INACTIVE", "SUSPENDED", "DELETED").contains(status)) {throw new RuntimeException("Invalid status: " + status);}userRepository.bulkUpdateUserStatus(ids, status);}/*** 根据条件统计用户数*/public Long countUsers(Map<String, Object> criteria) {return userRepository.countUsersByCriteria(criteria);}/*** 验证用户是否存在*/public boolean userExists(String id) {return userRepository.existsById(id);}/*** 验证邮箱是否已存在*/public boolean emailExists(String email) {return userRepository.existsByEmail(email);}
}
4.2 高级 Service(包含事务和异常处理)
package com.example.mongodbdemo.service;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.exception.ResourceNotFoundException;
import com.example.mongodbdemo.exception.ValidationException;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;/*** 高级用户服务(包含重试机制、事务管理等)*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AdvancedUserService {private final UserRepository userRepository;private final MongoTemplate mongoTemplate;/*** 创建用户(带重试机制)*/@Retryable(value = {DuplicateKeyException.class},maxAttempts = 3,backoff = @Backoff(delay = 1000, multiplier = 2))@Transactionalpublic User createUserWithRetry(User user) {try {user.setId(null);user.setCreatedAt(LocalDateTime.now());user.setUpdatedAt(LocalDateTime.now());return userRepository.save(user);} catch (DuplicateKeyException e) {log.error("Duplicate key error when creating user", e);throw new ValidationException("Email already exists");}}/*** 使用 MongoTemplate 执行复杂操作*/@Transactionalpublic void updateUserWithMongoTemplate(String userId, Map<String, Object> updates) {Query query = new Query(Criteria.where("id").is(userId));User existingUser = mongoTemplate.findOne(query, User.class);if (existingUser == null) {throw new ResourceNotFoundException("User not found");}Update update = new Update();updates.forEach((key, value) -> {switch (key) {case "firstName":update.set("first_name", value);break;case "lastName":update.set("last_name", value);break;case "age":update.set("age", value);break;case "email":// 验证邮箱是否唯一if (!existingUser.getEmail().equals(value)) {checkEmailUniqueness((String) value);update.set("email", value);}break;}});update.set("updated_at", LocalDateTime.now());mongoTemplate.updateFirst(query, update, User.class);}/*** 使用事务执行多个操作*/@Transactionalpublic void executeInTransaction(List<Runnable> operations) {operations.forEach(Runnable::run);}/*** 批量操作*/@Transactionalpublic void batchInsert(List<User> users) {users.forEach(user -> {user.setId(null);user.setCreatedAt(LocalDateTime.now());user.setUpdatedAt(LocalDateTime.now());});mongoTemplate.insertAll(users);}/*** 使用聚合查询*/public Map<String, Object> getUserAnalytics() {List<Map> cityStats = userRepository.getUserStatistics();List<UserRepository.CityStats> ageStats = userRepository.countUsersByCity();return Map.of("cityStatistics", cityStats,"ageStatistics", ageStats,"totalUsers", userRepository.count(),"activeUsers", userRepository.countByStatus("ACTIVE"));}/*** 地理空间查询*/public List<User> findUsersNearLocation(double longitude, double latitude, double maxDistance) {Query query = new Query(Criteria.where("location").nearSphere(new org.springframework.data.geo.Point(longitude, latitude)).maxDistance(maxDistance));return mongoTemplate.find(query, User.class);}/*** 文本搜索*/public List<User> searchUsersByText(String searchText) {Query query = new Query(Criteria.where("$text").matching(new org.springframework.data.mongodb.core.query.TextCriteria().matching(searchText)));query.limit(50);return mongoTemplate.find(query, User.class);}private void checkEmailUniqueness(String email) {Query query = new Query(Criteria.where("email").is(email));long count = mongoTemplate.count(query, User.class);if (count > 0) {throw new ValidationException("Email already exists");}}
}
五、Controller 层
5.1 RESTful API 控制器
package com.example.mongodbdemo.controller;import com.example.mongodbdemo.dto.*;
import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.util.List;
import java.util.Map;/*** 用户管理API*/
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
@Validated
@Tag(name = "用户管理", description = "用户管理相关API")
public class UserController {private final UserService userService;/*** 创建用户*/@PostMapping@Operation(summary = "创建用户", description = "创建新用户")public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody CreateUserRequest request) {log.info("Creating user with email: {}", request.getEmail());User user = convertToEntity(request);User createdUser = userService.createUser(user);ApiResponse<User> response = ApiResponse.success(createdUser,"用户创建成功");return ResponseEntity.status(HttpStatus.CREATED).body(response);}/*** 批量创建用户*/@PostMapping("/batch")@Operation(summary = "批量创建用户")public ResponseEntity<ApiResponse<List<User>>> createUsers(@Valid @RequestBody List<CreateUserRequest> requests) {log.info("Creating {} users", requests.size());List<User> users = requests.stream().map(this::convertToEntity).toList();List<User> createdUsers = userService.createUsers(users);ApiResponse<List<User>> response = ApiResponse.success(createdUsers,String.format("成功创建 %d 个用户", createdUsers.size()));return ResponseEntity.status(HttpStatus.CREATED).body(response);}/*** 获取用户详情*/@GetMapping("/{id}")@Operation(summary = "获取用户详情", description = "根据ID获取用户信息")public ResponseEntity<ApiResponse<User>> getUserById(@PathVariable @Parameter(description = "用户ID", required = true) String id) {log.info("Getting user by ID: {}", id);return userService.getUserById(id).map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功"))).orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error("用户不存在")));}/*** 根据邮箱获取用户*/@GetMapping("/email/{email}")@Operation(summary = "根据邮箱获取用户")public ResponseEntity<ApiResponse<User>> getUserByEmail(@PathVariable String email) {log.info("Getting user by email: {}", email);return userService.getUserByEmail(email).map(user -> ResponseEntity.ok(ApiResponse.success(user, "获取成功"))).orElse(ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error("用户不存在")));}/*** 获取所有用户*/@GetMapping@Operation(summary = "获取用户列表", description = "获取所有用户,支持分页和排序")public ResponseEntity<ApiResponse<Page<User>>> getAllUsers(@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {log.info("Getting all users with pageable: {}", pageable);Page<User> users = userService.getUsers(pageable);ApiResponse<Page<User>> response = ApiResponse.success(users,String.format("获取到 %d 个用户", users.getTotalElements()));return ResponseEntity.ok(response);}/*** 搜索用户*/@GetMapping("/search")@Operation(summary = "搜索用户", description = "根据条件搜索用户")public ResponseEntity<ApiResponse<Page<User>>> searchUsers(@RequestParam(required = false) String firstName,@RequestParam(required = false) String lastName,@RequestParam(required = false) String email,@RequestParam(required = false) Integer ageFrom,@RequestParam(required = false) Integer ageTo,@RequestParam(required = false) String city,@RequestParam(required = false) String status,@PageableDefault(size = 20, sort = "createdAt") Pageable pageable) {log.info("Searching users with criteria");Map<String, Object> criteria = new java.util.HashMap<>();if (firstName != null) criteria.put("firstName", firstName);if (lastName != null) criteria.put("lastName", lastName);if (email != null) criteria.put("email", email);if (ageFrom != null) criteria.put("ageFrom", ageFrom);if (ageTo != null) criteria.put("ageTo", ageTo);if (city != null) criteria.put("city", city);if (status != null) criteria.put("status", status);Page<User> users = userService.searchUsers(criteria, pageable);ApiResponse<Page<User>> response = ApiResponse.success(users,String.format("搜索到 %d 个用户", users.getTotalElements()));return ResponseEntity.ok(response);}/*** 更新用户*/@PutMapping("/{id}")@Operation(summary = "更新用户", description = "更新用户信息")public ResponseEntity<ApiResponse<User>> updateUser(@PathVariable String id,@Valid @RequestBody UpdateUserRequest request) {log.info("Updating user with ID: {}", id);User userUpdates = convertToEntity(request);User updatedUser = userService.updateUser(id, userUpdates);ApiResponse<User> response = ApiResponse.success(updatedUser,"用户更新成功");return ResponseEntity.ok(response);}/*** 部分更新用户*/@PatchMapping("/{id}")@Operation(summary = "部分更新用户", description = "更新用户部分字段")public ResponseEntity<ApiResponse<Void>> partialUpdateUser(@PathVariable String id,@RequestBody Map<String, Object> updates) {log.info("Partial updating user with ID: {}", id);userService.partialUpdateUser(id, updates);return ResponseEntity.ok(ApiResponse.success("用户更新成功"));}/*** 删除用户*/@DeleteMapping("/{id}")@Operation(summary = "删除用户", description = "根据ID删除用户")public ResponseEntity<ApiResponse<Void>> deleteUser(@PathVariable String id) {log.info("Deleting user with ID: {}", id);userService.deleteUser(id);return ResponseEntity.ok(ApiResponse.success("用户删除成功"));}/*** 软删除用户*/@DeleteMapping("/{id}/soft")@Operation(summary = "软删除用户")public ResponseEntity<ApiResponse<Void>> softDeleteUser(@PathVariable String id) {log.info("Soft deleting user with ID: {}", id);userService.softDeleteUser(id);return ResponseEntity.ok(ApiResponse.success("用户已标记为删除"));}/*** 获取用户统计信息*/@GetMapping("/statistics")@Operation(summary = "获取用户统计信息")public ResponseEntity<ApiResponse<List<Map>>> getUserStatistics() {log.info("Getting user statistics");List<Map> statistics = userService.getUserStatistics();ApiResponse<List<Map>> response = ApiResponse.success(statistics,"获取统计信息成功");return ResponseEntity.ok(response);}/*** 获取所有城市*/@GetMapping("/cities")@Operation(summary = "获取所有城市")public ResponseEntity<ApiResponse<List<String>>> getAllCities() {log.info("Getting all cities");List<String> cities = userService.getAllCities();ApiResponse<List<String>> response = ApiResponse.success(cities,String.format("获取到 %d 个城市", cities.size()));return ResponseEntity.ok(response);}/*** 验证邮箱是否可用*/@GetMapping("/check-email")@Operation(summary = "验证邮箱是否可用")public ResponseEntity<ApiResponse<Boolean>> checkEmailAvailability(@RequestParam String email) {log.info("Checking email availability: {}", email);boolean available = !userService.emailExists(email);String message = available ? "邮箱可用" : "邮箱已被使用";return ResponseEntity.ok(ApiResponse.success(available, message));}/*** 根据条件查询用户*/@GetMapping("/query")@Operation(summary = "根据条件查询用户")public ResponseEntity<ApiResponse<List<User>>> queryUsers(@RequestParam(required = false) String firstName,@RequestParam(required = false) @Min(0) Integer minAge,@RequestParam(required = false) String city) {log.info("Querying users with criteria");List<User> users = userService.findUsersByCriteria(firstName, minAge, city);ApiResponse<List<User>> response = ApiResponse.success(users,String.format("查询到 %d 个用户", users.size()));return ResponseEntity.ok(response);}/*** 更新用户状态*/@PutMapping("/{id}/status")@Operation(summary = "更新用户状态")public ResponseEntity<ApiResponse<Void>> updateUserStatus(@PathVariable String id,@RequestParam String status) {log.info("Updating user status, ID: {}, status: {}", id, status);userService.updateUserStatus(id, status);return ResponseEntity.ok(ApiResponse.success("用户状态更新成功"));}/*** 批量更新用户状态*/@PutMapping("/batch-status")@Operation(summary = "批量更新用户状态")public ResponseEntity<ApiResponse<Void>> bulkUpdateUserStatus(@RequestParam List<String> ids,@RequestParam String status) {log.info("Bulk updating user status for {} users", ids.size());userService.bulkUpdateUserStatus(ids, status);return ResponseEntity.ok(ApiResponse.success("批量更新成功"));}/*** 根据条件统计用户数*/@GetMapping("/count")@Operation(summary = "根据条件统计用户数")public ResponseEntity<ApiResponse<Long>> countUsers(@RequestParam(required = false) Integer ageFrom,@RequestParam(required = false) Integer ageTo,@RequestParam(required = false) String city,@RequestParam(required = false) String status) {log.info("Counting users with criteria");Map<String, Object> criteria = new java.util.HashMap<>();if (ageFrom != null) criteria.put("ageFrom", ageFrom);if (ageTo != null) criteria.put("ageTo", ageTo);if (city != null) criteria.put("city", city);if (status != null) criteria.put("status", status);Long count = userService.countUsers(criteria);return ResponseEntity.ok(ApiResponse.success(count, "统计完成"));}/*** 转换请求为实体*/private User convertToEntity(CreateUserRequest request) {return User.builder().firstName(request.getFirstName()).lastName(request.getLastName()).email(request.getEmail()).age(request.getAge()).phoneNumber(request.getPhoneNumber()).gender(request.getGender()).birthDate(request.getBirthDate()).city(request.getCity()).country(request.getCountry()).roles(request.getRoles()).preferences(request.getPreferences()).addresses(request.getAddresses()).build();}private User convertToEntity(UpdateUserRequest request) {return User.builder().firstName(request.getFirstName()).lastName(request.getLastName()).age(request.getAge()).phoneNumber(request.getPhoneNumber()).city(request.getCity()).country(request.getCountry()).build();}
}
六、DTO 和请求/响应对象
6.1 请求对象
package com.example.mongodbdemo.dto;import com.example.mongodbdemo.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;/*** 创建用户请求*/
@Data
@Schema(description = "创建用户请求")
public class CreateUserRequest {@NotBlank(message = "First name is required")@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")@Schema(description = "First name", example = "John", required = true)private String firstName;@NotBlank(message = "Last name is required")@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")@Schema(description = "Last name", example = "Doe", required = true)private String lastName;@NotBlank(message = "Email is required")@Email(message = "Invalid email format")@Schema(description = "Email address", example = "john.doe@example.com", required = true)private String email;@NotNull(message = "Age is required")@Min(value = 0, message = "Age must be positive")@Max(value = 150, message = "Age must be less than 150")@Schema(description = "Age", example = "25", required = true)private Integer age;@Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")@Schema(description = "Phone number", example = "+1-234-567-8900")private String phoneNumber;@Pattern(regexp = "^(MALE|FEMALE|OTHER)$", message = "Invalid gender")@Schema(description = "Gender", example = "MALE")private String gender;@Schema(description = "Birth date")private LocalDateTime birthDate;@Schema(description = "City", example = "New York")private String city;@Schema(description = "Country", example = "USA")private String country;@Schema(description = "User roles")private List<String> roles;@Schema(description = "User preferences")private User.Preferences preferences;@Schema(description = "User addresses")private List<User.Address> addresses;
}/*** 更新用户请求*/
@Data
@Schema(description = "更新用户请求")
public class UpdateUserRequest {@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")@Schema(description = "First name", example = "John")private String firstName;@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")@Schema(description = "Last name", example = "Doe")private String lastName;@Min(value = 0, message = "Age must be positive")@Max(value = 150, message = "Age must be less than 150")@Schema(description = "Age", example = "26")private Integer age;@Pattern(regexp = "^[0-9\\-+()\\s]*$", message = "Invalid phone number format")@Schema(description = "Phone number", example = "+1-234-567-8901")private String phoneNumber;@Schema(description = "City", example = "Los Angeles")private String city;@Schema(description = "Country", example = "USA")private String country;
}/*** 搜索用户请求*/
@Data
@Schema(description = "搜索用户请求")
public class SearchUserRequest {@Schema(description = "First name", example = "John")private String firstName;@Schema(description = "Last name", example = "Doe")private String lastName;@Email(message = "Invalid email format")@Schema(description = "Email", example = "john@example.com")private String email;@Schema(description = "Minimum age", example = "18")private Integer minAge;@Schema(description = "Maximum age", example = "60")private Integer maxAge;@Schema(description = "City", example = "New York")private String city;@Schema(description = "Status", example = "ACTIVE")private String status;@Schema(description = "Page number", example = "0")private Integer page;@Schema(description = "Page size", example = "20")private Integer size;@Schema(description = "Sort field", example = "createdAt")private String sort;@Schema(description = "Sort direction", example = "DESC")private String direction;
}
6.2 响应对象
package com.example.mongodbdemo.dto;import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;/*** 统一API响应*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "API响应")
public class ApiResponse<T> {@Schema(description = "是否成功", example = "true")private boolean success;@Schema(description = "响应消息", example = "操作成功")private String message;@Schema(description = "响应数据")private T data;@Schema(description = "错误代码")private String errorCode;@Schema(description = "时间戳", example = "2024-01-15T10:30:00")private LocalDateTime timestamp;/*** 成功响应*/public static <T> ApiResponse<T> success(T data, String message) {return ApiResponse.<T>builder().success(true).message(message).data(data).timestamp(LocalDateTime.now()).build();}public static <T> ApiResponse<T> success(T data) {return success(data, "操作成功");}public static ApiResponse<Void> success(String message) {return success(null, message);}/*** 失败响应*/public static <T> ApiResponse<T> error(String message, String errorCode) {return ApiResponse.<T>builder().success(false).message(message).errorCode(errorCode).timestamp(LocalDateTime.now()).build();}public static <T> ApiResponse<T> error(String message) {return error(message, null);}
}/*** 用户响应DTO(展示如何控制返回字段)*/
@Data
@Builder
@Schema(description = "用户响应")
public class UserResponse {@Schema(description = "用户ID", example = "507f1f77bcf86cd799439011")private String id;@Schema(description = "First name", example = "John")private String firstName;@Schema(description = "Last name", example = "Doe")private String lastName;@Schema(description = "Email", example = "john.doe@example.com")private String email;@Schema(description = "Age", example = "25")private Integer age;@Schema(description = "City", example = "New York")private String city;@Schema(description = "Country", example = "USA")private String country;@Schema(description = "Status", example = "ACTIVE")private String status;@Schema(description = "创建时间")private LocalDateTime createdAt;/*** 从实体转换*/public static UserResponse fromEntity(com.example.mongodbdemo.entity.User user) {return UserResponse.builder().id(user.getId()).firstName(user.getFirstName()).lastName(user.getLastName()).email(user.getEmail()).age(user.getAge()).city(user.getCity()).country(user.getCountry()).status(user.getStatus()).createdAt(user.getCreatedAt()).build();}
}
七、配置类
7.1 MongoDB 配置
package com.example.mongodbdemo.config;import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;/*** MongoDB配置类*/
@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongodbdemo.repository")
@Slf4j
public class MongoConfig extends AbstractMongoClientConfiguration {@Value("${spring.data.mongodb.host:localhost}")private String host;@Value("${spring.data.mongodb.port:27017}")private int port;@Value("${spring.data.mongodb.database:mydatabase}")private String database;@Value("${spring.data.mongodb.username:}")private String username;@Value("${spring.data.mongodb.password:}")private String password;@Value("${spring.data.mongodb.authentication-database:admin}")private String authenticationDatabase;@Overrideprotected String getDatabaseName() {return database;}@Overridepublic MongoClient mongoClient() {log.info("Connecting to MongoDB: {}:{}, database: {}", host, port, database);String connectionString;if (username.isEmpty() || password.isEmpty()) {// 无认证连接connectionString = String.format("mongodb://%s:%d/%s", host, port, database);} else {// 带认证连接connectionString = String.format("mongodb://%s:%s@%s:%d/%s?authSource=%s",username, password, host, port, database, authenticationDatabase);}MongoClientSettings settings = MongoClientSettings.builder().applyConnectionString(new ConnectionString(connectionString)).applyToConnectionPoolSettings(builder -> builder.maxSize(100).minSize(10).maxWaitTimeMillis(120000)).applyToSocketSettings(builder -> builder.connectTimeout(10000, java.util.concurrent.TimeUnit.MILLISECONDS)).build();return MongoClients.create(settings);}/*** 配置事务管理器(MongoDB 4.0+ 支持事务)*/@Beanpublic MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {return new MongoTransactionManager(dbFactory);}/*** 移除 _class 字段*/@Bean@Overridepublic MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory,MongoCustomConversions customConversions,MongoMappingContext mappingContext) {MappingMongoConverter converter = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext);// 移除 _class 字段converter.setTypeMapper(new DefaultMongoTypeMapper(null));return converter;}/*** 自定义类型转换*/@Bean@Overridepublic MongoCustomConversions customConversions() {return new MongoCustomConversions(Arrays.asList(// LocalDateTime 转 Datenew org.springframework.core.convert.converter.Converter<LocalDateTime, Date>() {@Overridepublic Date convert(LocalDateTime source) {return Date.from(source.atZone(ZoneId.systemDefault()).toInstant());}},// Date 转 LocalDateTimenew org.springframework.core.convert.converter.Converter<Date, LocalDateTime>() {@Overridepublic LocalDateTime convert(Date source) {return source.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();}}));}/*** 启用索引自动创建*/@Overrideprotected boolean autoIndexCreation() {return true;}
}
7.2 审计配置
package com.example.mongodbdemo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;import java.util.Optional;/*** MongoDB审计配置*/
@Configuration
@EnableMongoAuditing // 启用审计功能
public class MongoAuditConfig {/*** 审计员信息提供者*/@Beanpublic AuditorAware<String> auditorProvider() {return () -> {// 从Spring Security获取当前用户Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication == null || !authentication.isAuthenticated()) {return Optional.of("system");}return Optional.ofNullable(authentication.getName());};}
}
7.3 序列化配置
package com.example.mongodbdemo.config;import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;import java.text.SimpleDateFormat;@Configuration
public class JacksonConfig {@Beanpublic ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();// 注册Java 8时间模块objectMapper.registerModule(new JavaTimeModule());// 禁用日期转时间戳objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);// 设置日期格式objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// 忽略未知属性objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);return objectMapper;}
}
八、异常处理
8.1 自定义异常
package com.example.mongodbdemo.exception;/*** 资源不存在异常*/
public class ResourceNotFoundException extends RuntimeException {public ResourceNotFoundException(String message) {super(message);}public ResourceNotFoundException(String message, Throwable cause) {super(message, cause);}
}/*** 验证异常*/
public class ValidationException extends RuntimeException {public ValidationException(String message) {super(message);}public ValidationException(String message, Throwable cause) {super(message, cause);}
}/*** 业务异常*/
public class BusinessException extends RuntimeException {private String errorCode;public BusinessException(String message) {super(message);}public BusinessException(String errorCode, String message) {super(message);this.errorCode = errorCode;}public String getErrorCode() {return errorCode;}
}
8.2 全局异常处理器
package com.example.mongodbdemo.handler;import com.example.mongodbdemo.dto.ApiResponse;
import com.example.mongodbdemo.exception.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;/*** 全局异常处理器*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 处理资源不存在异常*/@ExceptionHandler(ResourceNotFoundException.class)public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {log.error("Resource not found: {}", ex.getMessage());ApiResponse<Void> response = ApiResponse.error(ex.getMessage(),"RESOURCE_NOT_FOUND");return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);}/*** 处理验证异常*/@ExceptionHandler(ValidationException.class)public ResponseEntity<ApiResponse<Void>> handleValidationException(ValidationException ex, WebRequest request) {log.error("Validation error: {}", ex.getMessage());ApiResponse<Void> response = ApiResponse.error(ex.getMessage(),"VALIDATION_ERROR");return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}/*** 处理业务异常*/@ExceptionHandler(BusinessException.class)public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex, WebRequest request) {log.error("Business error: {}", ex.getMessage());ApiResponse<Void> response = ApiResponse.error(ex.getMessage(),ex.getErrorCode());return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}/*** 处理参数验证异常*/@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(MethodArgumentNotValidException ex) {Map<String, String> errors = new HashMap<>();ex.getBindingResult().getAllErrors().forEach(error -> {String fieldName = ((FieldError) error).getField();String errorMessage = error.getDefaultMessage();errors.put(fieldName, errorMessage);});ApiResponse<Map<String, String>> response = ApiResponse.error("参数验证失败","VALIDATION_FAILED");response.setData(errors);return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}/*** 处理约束违反异常*/@ExceptionHandler(ConstraintViolationException.class)public ResponseEntity<ApiResponse<Map<String, String>>> handleConstraintViolationException(ConstraintViolationException ex) {Map<String, String> errors = new HashMap<>();ex.getConstraintViolations().forEach(violation -> {String fieldName = violation.getPropertyPath().toString();String errorMessage = violation.getMessage();errors.put(fieldName, errorMessage);});ApiResponse<Map<String, String>> response = ApiResponse.error("参数约束违反","CONSTRAINT_VIOLATION");response.setData(errors);return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);}/*** 处理唯一键冲突异常*/@ExceptionHandler(DuplicateKeyException.class)public ResponseEntity<ApiResponse<Void>> handleDuplicateKeyException(DuplicateKeyException ex) {log.error("Duplicate key error: {}", ex.getMessage());ApiResponse<Void> response = ApiResponse.error("数据已存在,请勿重复添加","DUPLICATE_KEY");return ResponseEntity.status(HttpStatus.CONFLICT).body(response);}/*** 处理数据访问异常*/@ExceptionHandler(DataAccessException.class)public ResponseEntity<ApiResponse<Void>> handleDataAccessException(DataAccessException ex) {log.error("Data access error: {}", ex.getMessage(), ex);ApiResponse<Void> response = ApiResponse.error("数据访问异常,请稍后重试","DATA_ACCESS_ERROR");return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}/*** 处理其他所有异常*/@ExceptionHandler(Exception.class)public ResponseEntity<ApiResponse<Void>> handleGlobalException(Exception ex, WebRequest request) {log.error("Unexpected error: {}", ex.getMessage(), ex);ApiResponse<Void> response = ApiResponse.error("服务器内部错误,请稍后重试","INTERNAL_SERVER_ERROR");return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}
}
九、测试
9.1 单元测试
package com.example.mongodbdemo.service;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;import java.time.LocalDateTime;
import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;@ExtendWith(MockitoExtension.class)
class UserServiceTest {@Mockprivate UserRepository userRepository;@InjectMocksprivate UserService userService;private User testUser;@BeforeEachvoid setUp() {testUser = User.builder().id("123").firstName("John").lastName("Doe").email("john.doe@example.com").age(25).city("New York").status("ACTIVE").createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();}@Testvoid testCreateUser_Success() {// Arrangewhen(userRepository.existsByEmail(any())).thenReturn(false);when(userRepository.save(any(User.class))).thenReturn(testUser);// ActUser createdUser = userService.createUser(testUser);// AssertassertNotNull(createdUser);assertEquals("John", createdUser.getFirstName());assertEquals("Doe", createdUser.getLastName());assertEquals("john.doe@example.com", createdUser.getEmail());verify(userRepository).existsByEmail("john.doe@example.com");verify(userRepository).save(any(User.class));}@Testvoid testCreateUser_EmailExists() {// Arrangewhen(userRepository.existsByEmail(any())).thenReturn(true);// Act & AssertassertThrows(RuntimeException.class, () -> {userService.createUser(testUser);});verify(userRepository).existsByEmail("john.doe@example.com");verify(userRepository, never()).save(any(User.class));}@Testvoid testGetUserById_Found() {// Arrangewhen(userRepository.findById("123")).thenReturn(Optional.of(testUser));// ActOptional<User> result = userService.getUserById("123");// AssertassertTrue(result.isPresent());assertEquals("John", result.get().getFirstName());verify(userRepository).findById("123");}@Testvoid testGetUserById_NotFound() {// Arrangewhen(userRepository.findById("999")).thenReturn(Optional.empty());// ActOptional<User> result = userService.getUserById("999");// AssertassertFalse(result.isPresent());verify(userRepository).findById("999");}@Testvoid testUpdateUser_Success() {// ArrangeUser updatedUser = User.builder().firstName("Jane").lastName("Smith").age(26).build();when(userRepository.findById("123")).thenReturn(Optional.of(testUser));when(userRepository.save(any(User.class))).thenAnswer(invocation -> {User user = invocation.getArgument(0);user.setFirstName("Jane");user.setLastName("Smith");user.setAge(26);return user;});// ActUser result = userService.updateUser("123", updatedUser);// AssertassertEquals("Jane", result.getFirstName());assertEquals("Smith", result.getLastName());assertEquals(26, result.getAge());assertNotNull(result.getUpdatedAt());verify(userRepository).findById("123");verify(userRepository).save(any(User.class));}@Testvoid testDeleteUser_Success() {// Arrangewhen(userRepository.existsById("123")).thenReturn(true);doNothing().when(userRepository).deleteById("123");// ActuserService.deleteUser("123");// Assertverify(userRepository).existsById("123");verify(userRepository).deleteById("123");}@Testvoid testDeleteUser_NotFound() {// Arrangewhen(userRepository.existsById("999")).thenReturn(false);// Act & AssertassertThrows(RuntimeException.class, () -> {userService.deleteUser("999");});verify(userRepository).existsById("999");verify(userRepository, never()).deleteById(any());}
}
9.2 集成测试
package com.example.mongodbdemo.integration;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ActiveProfiles;import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;import static org.junit.jupiter.api.Assertions.*;@DataMongoTest
@ActiveProfiles("test")
class UserRepositoryIntegrationTest {@Autowiredprivate UserRepository userRepository;private User user1;private User user2;private User user3;@BeforeEachvoid setUp() {// 清理数据userRepository.deleteAll();// 准备测试数据user1 = User.builder().firstName("John").lastName("Doe").email("john.doe@example.com").age(25).city("New York").status("ACTIVE").registrationDate(LocalDateTime.now()).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();user2 = User.builder().firstName("Jane").lastName("Smith").email("jane.smith@example.com").age(30).city("Los Angeles").status("ACTIVE").registrationDate(LocalDateTime.now().minusDays(1)).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();user3 = User.builder().firstName("Bob").lastName("Johnson").email("bob.johnson@example.com").age(35).city("New York").status("INACTIVE").registrationDate(LocalDateTime.now().minusDays(2)).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();userRepository.saveAll(Arrays.asList(user1, user2, user3));}@AfterEachvoid tearDown() {userRepository.deleteAll();}@Testvoid testSaveAndFindById() {// ArrangeUser newUser = User.builder().firstName("Alice").lastName("Brown").email("alice.brown@example.com").age(28).city("Chicago").status("ACTIVE").build();// ActUser savedUser = userRepository.save(newUser);Optional<User> foundUser = userRepository.findById(savedUser.getId());// AssertassertTrue(foundUser.isPresent());assertEquals("Alice", foundUser.get().getFirstName());assertEquals("Brown", foundUser.get().getLastName());assertEquals("alice.brown@example.com", foundUser.get().getEmail());}@Testvoid testFindByEmail() {// ActOptional<User> result = userRepository.findByEmail("john.doe@example.com");// AssertassertTrue(result.isPresent());assertEquals("John", result.get().getFirstName());assertEquals("Doe", result.get().getLastName());}@Testvoid testFindByCity() {// ActList<User> users = userRepository.findByCity("New York");// AssertassertEquals(2, users.size());assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));}@Testvoid testFindByAgeGreaterThan() {// ActList<User> users = userRepository.findByAgeGreaterThan(28);// AssertassertEquals(2, users.size());assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Bob")));}@Testvoid testFindByStatus() {// ActPageable pageable = PageRequest.of(0, 10);Page<User> activeUsers = userRepository.findByStatus("ACTIVE", pageable);// AssertassertEquals(2, activeUsers.getTotalElements());assertEquals(2, activeUsers.getContent().size());}@Testvoid testExistsByEmail() {// Actboolean exists = userRepository.existsByEmail("john.doe@example.com");boolean notExists = userRepository.existsByEmail("nonexistent@example.com");// AssertassertTrue(exists);assertFalse(notExists);}@Testvoid testCountByCity() {// ActLong count = userRepository.countByCity("New York");// AssertassertEquals(2, count);}@Testvoid testFindUsersByAgeRange() {// ActList<User> users = userRepository.findUsersByAgeRange(25, 35);// AssertassertEquals(2, users.size());assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("John")));assertTrue(users.stream().anyMatch(u -> u.getFirstName().equals("Jane")));}@Testvoid testDeleteByEmail() {// ActuserRepository.deleteByEmail("john.doe@example.com");// AssertOptional<User> deletedUser = userRepository.findByEmail("john.doe@example.com");assertFalse(deletedUser.isPresent());}
}
十、应用启动和配置
10.1 主启动类
package com.example.mongodbdemo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication
@EnableMongoAuditing // 启用审计功能
@EnableAsync // 启用异步支持
@EnableScheduling // 启用定时任务
@EnableConfigurationProperties // 启用配置属性
public class MongodbDemoApplication {public static void main(String[] args) {SpringApplication.run(MongodbDemoApplication.class, args);}
}
10.2 初始化数据
package com.example.mongodbdemo.init;import com.example.mongodbdemo.entity.User;
import com.example.mongodbdemo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;import java.time.LocalDateTime;
import java.util.Arrays;/*** 初始化数据(仅用于开发环境)*/
@Configuration
@RequiredArgsConstructor
@Slf4j
@Profile("dev") // 只在开发环境运行
public class DataInitializer {private final UserRepository userRepository;@Beanpublic CommandLineRunner initData() {return args -> {// 清理现有数据userRepository.deleteAll();// 创建测试用户User admin = User.builder().firstName("Admin").lastName("User").email("admin@example.com").age(30).city("Beijing").country("China").status("ACTIVE").roles(Arrays.asList("ADMIN", "USER")).registrationDate(LocalDateTime.now()).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();User testUser = User.builder().firstName("Test").lastName("User").email("test@example.com").age(25).city("Shanghai").country("China").status("ACTIVE").roles(Arrays.asList("USER")).registrationDate(LocalDateTime.now()).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();userRepository.saveAll(Arrays.asList(admin, testUser));log.info("Initialized {} users", userRepository.count());};}
}
十一、Swagger API文档
package com.example.mongodbdemo.config;import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.List;@Configuration
public class SwaggerConfig {@Value("${server.servlet.context-path:/api}")private String contextPath;@Beanpublic OpenAPI customOpenAPI() {return new OpenAPI().info(new Info().title("MongoDB Demo API").description("Spring Boot MongoDB 集成示例").version("1.0.0").contact(new Contact().name("开发团队").email("dev@example.com")).license(new License().name("Apache 2.0").url("http://springdoc.org"))).servers(List.of(new Server().url(contextPath).description("API Server"))).components(new io.swagger.v3.oas.models.Components().addSecuritySchemes("bearer-key",new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")));}@Beanpublic GroupedOpenApi publicApi() {return GroupedOpenApi.builder().group("public").pathsToMatch("/api/**").build();}@Beanpublic GroupedOpenApi adminApi() {return GroupedOpenApi.builder().group("admin").pathsToMatch("/api/admin/**").build();}
}
十二、部署和配置建议
12.1 生产环境配置
# application-prod.yml
spring:data:mongodb:uri: ${MONGODB_URI:mongodb://username:password@mongodb-host:27017/database?authSource=admin&replicaSet=rs0}auto-index-creation: true# 连接池配置mongodb:connection-pool:max-size: 200min-size: 20max-wait-time: 300000# 性能优化
server:tomcat:max-threads: 200min-spare-threads: 20# 监控
management:endpoints:web:exposure:include: health,info,metrics,prometheusmetrics:export:prometheus:enabled: true
12.2 健康检查端点
package com.example.mongodbdemo.health;import com.mongodb.client.MongoClient;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;@Component
@RequiredArgsConstructor
public class MongoHealthIndicator implements HealthIndicator {private final MongoTemplate mongoTemplate;private final MongoClient mongoClient;@Overridepublic Health health() {try {// 执行简单的MongoDB命令检查连接mongoTemplate.executeCommand("{ ping: 1 }");// 获取服务器状态var serverStatus = mongoClient.getClusterDescription();return Health.up().withDetail("clusterType", serverStatus.getType()).withDetail("servers", serverStatus.getServerDescriptions().size()).build();} catch (Exception e) {return Health.down().withDetail("error", e.getMessage()).build();}}
}
十三、性能优化建议
-
索引优化:
- 为常用查询字段创建索引
- 避免在频繁更新的字段上创建索引
- 使用复合索引覆盖查询
-
查询优化:
- 使用投影只返回需要的字段
- 避免在应用层进行大量数据处理
- 使用分页限制返回数据量
-
连接池优化:
- 根据并发量调整连接池大小
- 监控连接使用情况
-
监控和日志:
- 启用慢查询日志
- 监控MongoDB性能指标
- 使用Spring Boot Actuator