Skip to content

Spring Data MongoDB

什么是MongoDB?

MongoDB是一种面向文档的NoSQL数据库,它以JSON风格的BSON (Binary JSON) 格式存储数据,支持动态模式,便于数据表示和存储。MongoDB的主要特性包括高性能、高可用性、自动分片和丰富的查询语言。

Spring Data MongoDB介绍

Spring Data MongoDB是Spring Data家族的一个子项目,提供了对MongoDB数据库的集成支持。它简化了与MongoDB的交互,提供了一致的Spring编程模型,使得开发者能够快速构建基于MongoDB的应用。

主要特性

  1. 简化的Repository抽象:自动实现基本CRUD操作
  2. 基于注解的映射:使用@Document等注解将Java对象映射到MongoDB文档
  3. 模板操作:通过MongoTemplate提供丰富的数据库操作API
  4. 自动索引创建:基于实体类定义自动创建索引
  5. 聚合框架支持:支持MongoDB的聚合操作
  6. 地理空间查询:支持地理位置索引和查询
  7. 响应式编程支持:提供ReactiveMongoTemplate和ReactiveMongoRepository
  8. 查询方法生成:根据方法名自动生成查询

依赖配置

Maven

xml
<!-- Spring Boot方式 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<!-- 非Spring Boot方式 -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-mongodb</artifactId>
    <version>4.1.0</version>
</dependency>

配置MongoDB连接

Spring Boot配置

properties
# 单机连接
spring.data.mongodb.uri=mongodb://localhost:27017/testdb

# 或详细配置
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=testdb
spring.data.mongodb.username=user
spring.data.mongodb.password=secret
spring.data.mongodb.authentication-database=admin

# MongoDB集群配置
spring.data.mongodb.uri=mongodb://user:secret@mongo1.example.com:27017,mongo2.example.com:27017/testdb?replicaSet=rs0

Java配置

java
@Configuration
@EnableMongoRepositories(basePackages = "com.example.repository")
public class MongoConfig extends AbstractMongoClientConfiguration {
    
    @Override
    protected String getDatabaseName() {
        return "testdb";
    }
    
    @Override
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString("mongodb://localhost:27017/testdb");
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
                .applyConnectionString(connectionString)
                .build();
        
        return MongoClients.create(mongoClientSettings);
    }
    
    // 自定义转换器(可选)
    @Override
    public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory,
                                                    MongoCustomConversions customConversions,
                                                    MongoMappingContext mappingContext) {
        MappingMongoConverter converter = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null)); // 禁用_class字段
        return converter;
    }
}

实体映射

MongoDB文档映射到Java对象:

java
@Document(collection = "users")
public class User {
    
    @Id
    private String id;
    
    private String username;
    
    private String email;
    
    @Field("account_status")
    private String status;
    
    @CreatedDate
    private Date createdDate;
    
    @LastModifiedDate
    private Date lastModifiedDate;
    
    @DBRef
    private List<Role> roles;
    
    @Transient
    private Integer age; // 不会被持久化到MongoDB
    
    // 构造函数、getter和setter
}

@Document(collection = "roles")
public class Role {
    
    @Id
    private String id;
    
    private String name;
    
    // 构造函数、getter和setter
}

启用审计功能

java
@Configuration
@EnableMongoAuditing
public class MongoAuditingConfig {
    
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.of("SYSTEM");
    }
}

MongoRepository

使用MongoRepository接口自动实现CRUD操作:

java
public interface UserRepository extends MongoRepository<User, String> {
    
    // 根据方法名生成查询
    User findByUsername(String username);
    
    List<User> findByEmailEndingWith(String emailDomain);
    
    List<User> findByCreatedDateBetween(Date start, Date end);
    
    // 使用@Query注解自定义查询
    @Query("{ 'status' : ?0 }")
    List<User> findByStatus(String status);
    
    @Query("{ 'username' : { $regex: ?0, $options: 'i' } }")
    List<User> findByRegexpUsername(String regex);
    
    // 更新操作
    @Query("{ 'username' : ?0 }")
    @Update("{ '$set' : { 'status' : ?1 } }")
    void updateUserStatus(String username, String status);
    
    // 删除操作
    long deleteByStatus(String status);
    
    // 分页和排序
    Page<User> findByStatus(String status, Pageable pageable);
    
    // 计数
    long countByStatus(String status);
    
    // 检查存在
    boolean existsByEmail(String email);
}

使用MongoTemplate

对于更复杂的查询和操作,可以使用MongoTemplate:

java
@Service
public class UserService {
    
    private final MongoTemplate mongoTemplate;
    
    public UserService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }
    
    // 基本CRUD操作
    public User save(User user) {
        return mongoTemplate.save(user);
    }
    
    public User findById(String id) {
        return mongoTemplate.findById(id, User.class);
    }
    
    public List<User> findAll() {
        return mongoTemplate.findAll(User.class);
    }
    
    public void delete(User user) {
        mongoTemplate.remove(user);
    }
    
    // 使用Query和Criteria进行查询
    public List<User> findActiveUsersByEmailDomain(String domain) {
        Query query = new Query();
        query.addCriteria(Criteria.where("email").regex(domain + "$"))
             .addCriteria(Criteria.where("status").is("ACTIVE"));
        
        return mongoTemplate.find(query, User.class);
    }
    
    // 分页查询
    public Page<User> findPaginated(int page, int size) {
        Query query = new Query().with(PageRequest.of(page, size));
        long total = mongoTemplate.count(query, User.class);
        List<User> users = mongoTemplate.find(query, User.class);
        
        return new PageImpl<>(users, PageRequest.of(page, size), total);
    }
    
    // 更新操作
    public void updateStatus(String id, String newStatus) {
        Query query = new Query(Criteria.where("id").is(id));
        Update update = new Update().set("status", newStatus);
        
        mongoTemplate.updateFirst(query, update, User.class);
    }
    
    // 批量更新
    public void deactivateOldUsers(Date cutoffDate) {
        Query query = new Query(Criteria.where("lastModifiedDate").lt(cutoffDate));
        Update update = new Update().set("status", "INACTIVE");
        
        mongoTemplate.updateMulti(query, update, User.class);
    }
    
    // 聚合操作示例
    public List<UserStatusCount> countUsersByStatus() {
        TypedAggregation<User> aggregation = Aggregation.newAggregation(
            User.class,
            Aggregation.group("status").count().as("count"),
            Aggregation.project("count").and("status").previousOperation()
        );
        
        AggregationResults<UserStatusCount> results = 
            mongoTemplate.aggregate(aggregation, UserStatusCount.class);
            
        return results.getMappedResults();
    }
    
    // 嵌套文档查询
    public List<User> findByAddressCity(String city) {
        Query query = new Query(Criteria.where("address.city").is(city));
        return mongoTemplate.find(query, User.class);
    }
}

// 聚合结果类
public class UserStatusCount {
    private String status;
    private long count;
    
    // getter和setter
}

复杂查询示例

条件查询

java
public List<User> findByComplexCriteria(String username, List<String> statuses, Date startDate) {
    Query query = new Query();
    
    // 动态添加条件
    if (username != null) {
        query.addCriteria(Criteria.where("username").regex(username, "i"));
    }
    
    if (statuses != null && !statuses.isEmpty()) {
        query.addCriteria(Criteria.where("status").in(statuses));
    }
    
    if (startDate != null) {
        query.addCriteria(Criteria.where("createdDate").gte(startDate));
    }
    
    return mongoTemplate.find(query, User.class);
}

嵌套查询

java
@Document
public class Order {
    @Id
    private String id;
    
    private String orderNumber;
    
    private List<LineItem> items;
    
    // 其他字段和方法
}

public class LineItem {
    private String productId;
    private int quantity;
    private BigDecimal price;
    
    // getter和setter
}

// 查询包含特定产品的订单
public List<Order> findOrdersContainingProduct(String productId) {
    Query query = new Query(Criteria.where("items.productId").is(productId));
    return mongoTemplate.find(query, Order.class);
}

地理空间查询

java
@Document(collection = "restaurants")
public class Restaurant {
    @Id
    private String id;
    
    private String name;
    
    @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
    private GeoJsonPoint location;
    
    // 其他字段和方法
}

// 查询附近的餐厅
public List<Restaurant> findNearbyRestaurants(double latitude, double longitude, double maxDistance) {
    Point point = new Point(longitude, latitude);
    
    Query query = new Query(
        Criteria.where("location").nearSphere(point)
            .maxDistance(maxDistance / 6378.137) // 转换为弧度(km)
    );
    
    return mongoTemplate.find(query, Restaurant.class);
}

事务支持

MongoDB 4.0+版本支持多文档事务:

java
@Service
public class UserRoleService {
    
    private final MongoTemplate mongoTemplate;
    private final MongoTransactionManager transactionManager;
    
    public UserRoleService(MongoTemplate mongoTemplate, MongoTransactionManager transactionManager) {
        this.mongoTemplate = mongoTemplate;
        this.transactionManager = transactionManager;
    }
    
    @Transactional
    public void createUserWithRoles(User user, List<Role> roles) {
        // 所有操作都在一个事务中执行
        for (Role role : roles) {
            mongoTemplate.save(role);
        }
        
        user.setRoles(roles);
        mongoTemplate.save(user);
    }
    
    // 编程式事务
    public void transferCredits(String fromUserId, String toUserId, int amount) {
        TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
        
        txTemplate.execute(status -> {
            User fromUser = mongoTemplate.findById(fromUserId, User.class);
            User toUser = mongoTemplate.findById(toUserId, User.class);
            
            if (fromUser.getCredits() < amount) {
                throw new InsufficientCreditsException();
            }
            
            mongoTemplate.updateFirst(
                Query.query(Criteria.where("id").is(fromUserId)),
                new Update().inc("credits", -amount),
                User.class
            );
            
            mongoTemplate.updateFirst(
                Query.query(Criteria.where("id").is(toUserId)),
                new Update().inc("credits", amount),
                User.class
            );
            
            return null;
        });
    }
}

特殊操作

使用GridFS存储大型文件

java
@Service
public class FileService {
    
    private final GridFsTemplate gridFsTemplate;
    private final GridFsOperations operations;
    
    public FileService(GridFsTemplate gridFsTemplate, GridFsOperations operations) {
        this.gridFsTemplate = gridFsTemplate;
        this.operations = operations;
    }
    
    // 存储文件
    public String storeFile(String filename, InputStream content, String contentType) throws IOException {
        ObjectId id = gridFsTemplate.store(
            content, filename, contentType, new Document("metadata", "user file")
        );
        return id.toString();
    }
    
    // 检索文件
    public GridFSFile getFile(String id) {
        return gridFsTemplate.findOne(new Query(Criteria.where("_id").is(new ObjectId(id))));
    }
    
    // 获取文件内容
    public InputStream getFileContent(GridFSFile file) {
        return operations.getResource(file).getInputStream();
    }
    
    // 删除文件
    public void deleteFile(String id) {
        gridFsTemplate.delete(new Query(Criteria.where("_id").is(new ObjectId(id))));
    }
}

字段更新操作

java
public void updateUserFields(String userId, Map<String, Object> updates) {
    Update update = new Update();
    
    for (Map.Entry<String, Object> entry : updates.entrySet()) {
        update.set(entry.getKey(), entry.getValue());
    }
    
    mongoTemplate.updateFirst(
        Query.query(Criteria.where("id").is(userId)),
        update,
        User.class
    );
}

// 数组操作
public void addRoleToUser(String userId, Role role) {
    mongoTemplate.updateFirst(
        Query.query(Criteria.where("id").is(userId)),
        new Update().push("roles", role),
        User.class
    );
}

public void removeRoleFromUser(String userId, String roleId) {
    mongoTemplate.updateFirst(
        Query.query(Criteria.where("id").is(userId)),
        new Update().pull("roles", Query.query(Criteria.where("id").is(roleId))),
        User.class
    );
}

响应式MongoDB

Spring Data MongoDB还提供了响应式编程支持:

java
// 添加依赖
// spring-boot-starter-data-mongodb-reactive

@Configuration
@EnableReactiveMongoRepositories
public class ReactiveMongoConfig extends AbstractReactiveMongoConfiguration {
    
    @Override
    protected String getDatabaseName() {
        return "testdb";
    }
    
    @Override
    public MongoClient reactiveMongoClient() {
        return MongoClients.create("mongodb://localhost:27017");
    }
}

// 响应式Repository
public interface ReactiveUserRepository extends ReactiveMongoRepository<User, String> {
    
    Flux<User> findByStatus(String status);
    
    Mono<User> findByUsername(String username);
}

// 使用ReactiveMongoTemplate
@Service
public class ReactiveUserService {
    
    private final ReactiveMongoTemplate template;
    
    public ReactiveUserService(ReactiveMongoTemplate template) {
        this.template = template;
    }
    
    public Mono<User> save(User user) {
        return template.save(user);
    }
    
    public Mono<User> findById(String id) {
        return template.findById(id, User.class);
    }
    
    public Flux<User> findAll() {
        return template.findAll(User.class);
    }
    
    public Flux<User> findByStatusWithTemplate(String status) {
        return template.find(
            Query.query(Criteria.where("status").is(status)),
            User.class
        );
    }
}

性能优化

索引管理

java
@Document(collection = "users")
@CompoundIndex(name = "email_username", def = "{'email': 1, 'username': 1}")
public class User {
    
    @Id
    private String id;
    
    @Indexed(unique = true)
    private String username;
    
    @Indexed
    private String email;
    
    @TextIndexed
    private String bio;
    
    @Indexed(direction = IndexDirection.DESCENDING)
    private Date createdDate;
    
    // 其他字段和方法
}

// 手动创建索引
@PostConstruct
public void initIndices() {
    mongoTemplate.indexOps(User.class).ensureIndex(
        new Index().on("lastModifiedDate", Direction.DESC)
    );
    
    // 复合索引
    mongoTemplate.indexOps(User.class).ensureIndex(
        new CompoundIndexDefinition(new Document("address.city", 1).append("address.state", 1))
    );
    
    // TTL索引
    IndexDefinition indexDefinition = new Index("expiresAt", Direction.ASC).expire(60);
    mongoTemplate.indexOps(Session.class).ensureIndex(indexDefinition);
}

投影查询

java
// 只返回需要的字段
public List<User> findUserNamesAndEmails() {
    Query query = new Query();
    query.fields().include("username").include("email");
    
    return mongoTemplate.find(query, User.class);
}

批量操作

java
public void bulkUpdateUserStatus(List<String> userIds, String newStatus) {
    BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, User.class);
    
    for (String id : userIds) {
        Query query = Query.query(Criteria.where("id").is(id));
        Update update = new Update().set("status", newStatus);
        
        bulkOps.updateOne(query, update);
    }
    
    BulkWriteResult result = bulkOps.execute();
    System.out.println("Modified count: " + result.getModifiedCount());
}

典型应用场景

1. 内容管理系统

MongoDB适合存储结构灵活的内容数据:

java
@Document(collection = "articles")
public class Article {
    @Id
    private String id;
    
    private String title;
    private String content;
    private List<String> tags;
    private List<Comment> comments;
    private Map<String, Object> metadata; // 灵活的元数据
    
    // 其他字段和方法
}

2. 日志和事件存储

java
@Document(collection = "logs")
public class LogEntry {
    @Id
    private String id;
    
    @Indexed
    private Date timestamp;
    
    private String level;
    private String message;
    private Map<String, Object> data;
    
    // TTL索引可用于自动删除旧日志
}

3. 实时分析

java
// 使用MongoDB聚合管道进行实时统计
public List<DailyUserStats> getDailyActiveUsers(Date startDate, Date endDate) {
    TypedAggregation<LogEntry> aggregation = Aggregation.newAggregation(
        LogEntry.class,
        Aggregation.match(Criteria.where("timestamp").gte(startDate).lte(endDate)),
        Aggregation.project()
            .andExpression("dateToString('%Y-%m-%d', timestamp)").as("day")
            .andExpression("userId").as("userId"),
        Aggregation.group("day").addToSet("userId").as("userIds"),
        Aggregation.project("userIds")
            .and("day").previousOperation()
            .and("activeUsers").size("userIds"),
        Aggregation.sort(Sort.Direction.ASC, "day")
    );
    
    return mongoTemplate.aggregate(aggregation, DailyUserStats.class)
        .getMappedResults();
}

故障排除与最佳实践

常见问题

  1. _class字段:默认情况下,Spring Data MongoDB会添加_class字段存储类信息,可以通过自定义MongoConverter禁用

  2. 嵌套文档更新:嵌套文档的更新需要使用点符号,如update.set("address.city", "New York")

  3. DBRef vs 嵌入文档:根据业务需求选择关联方式

  4. ObjectId转换:处理MongoDB的ObjectId时,需要进行适当的类型转换

最佳实践

  1. 适当索引:创建合适的索引提高查询性能

  2. 文档大小控制:避免文档过大,MongoDB有16MB文档大小限制

  3. 使用投影:仅查询需要的字段减少数据传输

  4. 分批处理大结果集:使用分页或流式处理大数据集

  5. 规划分片键:对于需要分片的集合,选择合适的分片键

  6. 使用复合索引:为常见查询创建复合索引

总结

Spring Data MongoDB提供了一个全面的MongoDB集成解决方案,简化了应用程序与MongoDB的交互。通过其Repository抽象、模板操作和注解支持,开发者可以快速构建基于MongoDB的高性能、可扩展的应用程序。

从基本的CRUD操作到高级的聚合查询,从同步API到响应式编程,Spring Data MongoDB提供了全方位的支持,使得MongoDB在Spring生态系统中的使用变得简单而高效。