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 会自动配置对应的 CassandraTemplate
和 MongoTemplate
,但 当两者共存时,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