1. 简介

在实际项目中,我们经常会遇到一个应用需要对接多种数据库技术的场景。比如,用 MongoDB 存书本内容,用 Cassandra 记录借阅日志——这种“混合持久化”架构并不少见。

本文将深入探讨:当一个 Spring Boot 应用同时引入多个 Spring Data 模块时,如何正确配置和区分它们的 Repository

我们以一个简单的图书管理系统为例,集成 Spring Data MongoDB 和 Spring Data Cassandra,带你避开常见的“多数据源踩坑”。


2. 所需依赖

首先,在 pom.xml 中引入对应的 Spring Boot Starter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-cassandra</artifactId>
  <version>3.1.5</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
  <version>3.1.5</version>
</dependency>

✅ 这两个 Starter 会自动配置对应的 CassandraTemplateMongoTemplate,但 当两者共存时,Spring 会进入严格模式,必须显式指定每个 Repository 的归属,否则启动直接报错。


3. 数据库环境搭建

我们使用 Docker 快速启动 MongoDB 和 Cassandra 实例:

$ docker run --name mongo-db -d -p 27017:27017 mongo:latest
$ docker run --name cassandra-db -d -p 9042:9042 cassandra:latest

⚠️ 注意:

  • 端口映射是必须的,否则应用无法访问容器内的数据库。
  • MongoDB 无需预建库或集合(Collection),写入时自动创建。
  • Cassandra 不同:Keyspace 和表必须手动创建,Spring Data 不会自动帮你建表。

进入 Cassandra 容器并初始化 schema:

$ docker exec -it cassandra-db /bin/bash
root@419acd18891e:/# cqlsh
cqlsh> CREATE KEYSPACE IF NOT EXISTS baeldung 
       WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};
cqlsh> USE baeldung;
cqlsh> CREATE TABLE bookaudit(
         bookid VARCHAR,
         rentalrecno VARCHAR,
         loandate VARCHAR,
         loaner VARCHAR,
         PRIMARY KEY(bookid, rentalrecno)
       );

最后,配置 application.properties

# Cassandra 配置
spring.data.cassandra.username=cassandra
spring.data.cassandra.password=cassandra
spring.data.cassandra.keyspaceName=baeldung
spring.data.cassandra.contactPoints=localhost
spring.data.cassandra.port=9042

# MongoDB 配置
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=baeldung

4. 多模块 Repository 识别机制

当 Spring Boot 检测到 classpath 下有多个 Spring Data 模块(如 MongoDB + Cassandra),它会进入 严格 Repository 配置模式。此时,必须明确告诉 Spring:哪个 Repository 属于哪个数据库

Spring 提供了三种方式来解决这个问题。

4.1. 继承模块专属的 Repository 接口

最直接的方式:让 Repository 直接继承对应数据库的专用接口。

例如,Cassandra 用 CassandraRepository

public interface BookAuditRepository extends CassandraRepository<BookAudit, String> {
}

MongoDB 用 MongoRepository

public interface BookDocumentRepository extends MongoRepository<BookDocument, String> {
}

实体类定义(Cassandra):

public class BookAudit {
  private String bookId;
  private String rentalRecNo;
  private String loaner;
  private String loanDate;
  // standard getters and setters
}

实体类定义(MongoDB):

public class BookDocument {
  private String bookId;
  private String bookName;
  private String bookAuthor;
  private String content;
  // standard getters and setters
}

✅ 测试验证:

@Test
public void givenBookAudit_whenPersistWithBookAuditRepository_thenSuccess() {
  BookAudit bookAudit = new BookAudit("lorem", "ipsum", "Baeldung", "19:30/20.08.2017");
  bookAuditRepository.save(bookAudit);

  List<BookAudit> result = bookAuditRepository.findAll();
  assertThat(result.isEmpty(), is(false));
  assertThat(result.contains(bookAudit), is(true));
}

📌 原理:Spring 通过 Repository 继承的父接口类型(如 CassandraRepository)来判断它属于哪个模块。


4.2. 使用模块专属的注解标记实体类

如果你不想让 Repository 接口绑定死某个实现,可以继承通用的 CrudRepository,然后通过实体类上的注解来“暗示”Spring 该用哪个数据库。

Repository 定义:

public interface BookAuditCrudRepository extends CrudRepository<BookAudit, String> {
}
public interface BookDocumentCrudRepository extends CrudRepository<BookDocument, String> {
}

关键在于实体类上的注解:

  • Cassandra 实体用 @Table(来自 org.springframework.data.cassandra.core.mapping.Table
  • 主键字段用 @PrimaryKeyColumn
@Table
public class BookAudit {
  @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED)
  private String bookId;
  @PrimaryKeyColumn
  private String rentalRecNo;
  private String loaner;
  private String loanDate;
  // standard getters and setters
}

MongoDB 实体用 @Document

@Document
public class BookDocument {
  private String bookId;
  private String bookName;
  private String bookAuthor;
  private String content;
  // standard getters and setters
}

✅ 测试代码:

@Test
public void givenBookAudit_whenPersistWithBookDocumentCrudRepository_thenSuccess() {
  BookDocument bookDocument = new BookDocument("lorem", "Foundation", "Isaac Asimov", "Once upon a time ...");
  bookDocumentCrudRepository.save(bookDocument);

  Iterable<BookDocument> resultIterable = bookDocumentCrudRepository.findAll();
  List<BookDocument> result = StreamSupport.stream(resultIterable.spliterator(), false)
                                           .collect(Collectors.toList());
  assertThat(result.isEmpty(), is(false));
  assertThat(result.contains(bookDocument), is(true));
}

⚠️ 注意:CrudRepository.findAll() 返回的是 Iterable,不是 List,需要手动转一下。

📌 原理:Spring 会检查实体类上的注解(如 @Table@Document)来决定使用哪个 RepositoryFactoryBean


4.3. 基于包路径的 Repository 扫描(推荐)

最清晰、最可控的方式:通过包路径划分不同数据库的 Repository

使用 @EnableCassandraRepositories@EnableMongoRepositories 显式指定扫描路径:

@EnableCassandraRepositories(basePackages = "com.baeldung.multipledatamodules.cassandra")
@EnableMongoRepositories(basePackages = "com.baeldung.multipledatamodules.mongo")
public class SpringDataMultipleModules {

  public static void main(String[] args) {
    SpringApplication.run(SpringDataMultipleModules.class, args);
  }
}

目录结构建议:

src/main/java
 └── com.baeldung.multipledatamodules
      ├── cassandra
      │   ├── BookAuditRepository.java
      │   └── BookAudit.java
      └── mongo
          ├── BookDocumentRepository.java
          └── BookDocument.java

✅ 优点:

  • 结构清晰,职责分明
  • 不依赖继承或注解,自由度高
  • 团队协作时不易混淆

❌ 缺点:

  • 包结构被“绑定”,重构时需小心

📌 这是大型项目中最推荐的方式,简单粗暴,一劳永逸。


5. 总结

本文介绍了在 Spring Boot 中同时使用多个 Spring Data 模块的三种配置方式:

方式 适用场景 推荐度
继承模块专属 Repository 快速原型、简单项目 ⭐⭐⭐
实体类使用模块专属注解 想用 CrudRepository 通用接口 ⭐⭐⭐⭐
包路径扫描 + @EnableXxxRepositories 中大型项目、团队协作 ✅⭐⭐⭐⭐

📌 最佳实践建议

  • 优先使用 包路径扫描 方式,清晰可控
  • 实体类该加注解就加,比如 @Document@Table,这是元数据,不是累赘
  • 多数据源环境下,不要依赖自动配置的模糊匹配,显式声明才是王道

完整代码示例已上传至 GitHub:https://github.com/baeldung/spring-boot-tutorials/tree/master/spring-boot-data


原始标题:Repositories with Multiple Spring Data Modules | Baeldung