Files
admin-api/docs/datasource.md
T

162 lines
7.3 KiB
Markdown
Raw Normal View History

2026-08-31 16:52:43 +08:00
# 双数据源说明
本服务使用两套手工配置的 JPA 数据源,不依赖动态数据源框架。
| 侧 | 数据库 | 用途 | DAO 包 | Entity 包 | 事务管理器 |
|---|---|---|---|---|---|
| Primary | MySQL | 数据字典、UI 设置(读写) | `domain.primary.dao` | `domain.primary.entity` | `transactionManagerPrimary` |
| Secondary | Oracle | 机场基础数据、季度计划(当前只读) | `domain.secondary.dao` | `domain.secondary.entity` | `transactionManagerSecondary` |
> Secondary 没有技术上的强制只读保护;不要在其 DAO 上调用 `save()` 或 `delete()`,除非已确认 Oracle 写入需求和事务方案。
## 核心思路:启动时把 DAO 固定接到一套数据库
这不是按请求、用户或 SQL 内容动态选择数据源;**应用启动时,Spring 就已经为每个 DAO 创建好代理,并把它固定绑定到 Primary 或 Secondary。**
```text
SysAirlineDao
位于 domain.secondary.dao
↓ 被 SecondaryConfig 的 @EnableJpaRepositories 扫描
↓ 创建 Repository 代理,绑定 entityManagerFactorySecondary
↓ entityManagerFactorySecondary 使用 secondaryDataSource
↓ 此 DAO 的所有 JPA 操作都访问 Oracle
DatadicItemDao
位于 domain.primary.dao
↓ 被 PrimaryConfig 的 @EnableJpaRepositories 扫描
↓ 创建 Repository 代理,绑定 entityManagerFactoryPrimary
↓ entityManagerFactoryPrimary 使用 primaryDataSource
↓ 此 DAO 的所有 JPA 操作都访问 MySQL
```
Entity 的包路径服务于同一个绑定关系:Primary EMF 只认识 Primary EntitySecondary EMF 只认识 Secondary Entity。也就是说,一个 DAO 所操作的 Entity 必须被**同一套 EMF**管理。
> **一句话记忆:DAO 在哪个 `*.dao` 根包,就由该包对应的 Config 创建代理;该 Config 指向哪一个 EMF / DataSourceDAO 就访问哪一个数据库。**
Java 包不会让 JVM 把类“加载到不同地方”——所有类仍在同一个应用进程中。包名只是 Spring/JPA 的**扫描筛选条件**;真正完成数据库路由的是 `@EnableJpaRepositories`、EMF 和 DataSource 之间的显式引用。
## 不同 package 的类如何被装载和绑定
这里的“装载”分两件事:**Entity 注册到哪个 JPA EMF**,以及 **DAO 代理使用哪个 EMF/事务管理器**。它们不是靠类名或注解自动猜测,而是由两套 Config 中的扫描包明确指定。
### 1. Entity`.packages()` 决定注册到哪个 EMF
`PrimaryConfig` 创建 MySQL 的 EMF 时只扫描 Primary Entity 包:
```java
// PrimaryConfig.java
builder
.dataSource(primaryDataSource)
.packages("com.gzzn.omms.adminapi.domain.primary.entity")
.persistenceUnit("primaryPersistenceUnit")
.build();
```
`SecondaryConfig` 创建 Oracle 的 EMF 时只扫描 Secondary Entity 包:
```java
// SecondaryConfig.java
builder
.dataSource(secondaryDataSource)
.packages("com.gzzn.omms.adminapi.domain.secondary.entity")
.persistenceUnit("secondaryPersistenceUnit")
.build();
```
`.packages()` 会扫描指定包及其子包中的 `@Entity`(以及关联的 JPA 映射类)。所以:
- `domain.primary.entity.dictionary.DatadicItem` 会注册到 `primaryPersistenceUnit`,其 SQL 发往 MySQL。
- `domain.secondary.entity.baiscdata.SysAirline` 会注册到 `secondaryPersistenceUnit`,其 SQL 发往 Oracle。
- `baiscdata` 虽然是现有目录中的拼写,但它仍是 `domain.secondary.entity` 的子包,会被扫描到;**不要为了改名直接移动类**,除非同步验证所有引用和映射。
`@Entity` 本身不是普通 Spring Service Bean;它由对应 EMF 扫描并维护映射元数据。放错包时,它会被注册到错误一侧的 EMF,后续 SQL 会查错数据库。
### 2. DAO`@EnableJpaRepositories` 决定代理绑定到哪个 EMF
两个配置类分别扫描不同 DAO 包,并显式引用本侧的 EMF 和事务管理器:
```java
// PrimaryConfig.java
@EnableJpaRepositories(
basePackages = "com.gzzn.omms.adminapi.domain.primary.dao",
entityManagerFactoryRef = "entityManagerFactoryPrimary",
transactionManagerRef = "transactionManagerPrimary")
// SecondaryConfig.java
@EnableJpaRepositories(
basePackages = "com.gzzn.omms.adminapi.domain.secondary.dao",
entityManagerFactoryRef = "entityManagerFactorySecondary",
transactionManagerRef = "transactionManagerSecondary")
```
应用启动时,Spring Data 会扫描这些包中的 `CrudRepository` 接口并创建代理:
```text
primary.dao 中的 DAO → Primary EMF / Primary TM → MySQL
secondary.dao 中的 DAO → Secondary EMF / Secondary TM → Oracle
```
因此 Controller 或 Service 注入 `SysAirlineDao` 时,拿到的是已绑定 Oracle 的代理;业务调用方不需要、也不应根据 DAO 名称自行切换数据源。
### 3. 新增类时按此配对
```text
MySQL: primary.entity.<业务包>.XxxEntity
primary.dao.<业务包>.XxxDao
Oracle: secondary.entity.<业务包>.XxxEntity
secondary.dao.<业务包>.XxxDao
```
只要类放在上述根包或任意子包内,现有扫描配置即可发现它们,**无需新增扫描配置**。若要使用第三套根包或第三个数据源,才需要新增对应的 DataSource、EMF、事务管理器和 `@EnableJpaRepositories` 配置。
## 配置位置
| 内容 | 代码位置 |
|---|---|
| MySQL DataSource | `config/PrimaryDataSourceConfig.java` |
| Oracle DataSource | `config/SecondaryDataSourceConfig.java` |
| MySQL JPA / DAO 绑定 | `config/PrimaryConfig.java` |
| Oracle JPA / DAO 绑定 | `config/SecondaryConfig.java` |
| 环境连接信息 | `src/main/resources/application-{dev,test,pro}.yml` |
Primary 的 `DataSource``EntityManager`、EMF 和事务管理器均标记 `@Primary`。这只是在未明确指定 Bean 时提供默认值;Repository 实际使用哪一侧,仍由各自的 `@EnableJpaRepositories` 配置决定。
## 新增表访问的做法
### 新增 MySQL 表
1. Entity 放到 `domain.primary.entity` 下。
2. DAO 放到 `domain.primary.dao` 下。
3. 需要写操作时使用 `transactionManagerPrimary`
### 新增 Oracle 表
1. Entity 放到 `domain.secondary.entity` 下。
2. DAO 放到 `domain.secondary.dao` 下。
3. 默认按只读接口实现;若确实需要写操作,显式指定 `transactionManagerSecondary`,并先确认 Oracle 权限与业务影响。
## 常见错误
| 问题 | 后果 | 排查/处理 |
|---|---|---|
| Entity 或 DAO 放错侧 | 可能能启动,首次查询才报目标库不存在表 | 检查 Entity、DAO 包路径及两套 Config 的扫描包 |
| 只新增 DAO、未新增正确 Entity | JPA 找不到实体或映射不完整 | Entity 与 DAO 必须成对放在同侧 |
| 裸写 `@Transactional` | 默认使用 Primary 事务管理器 | 显式填写事务管理器名称 |
| 一个业务操作同时写两库 | 不具备原子性,可能部分成功 | 避免跨库写;需要原子性时引入分布式事务方案 |
| 漏配某环境数据源 | 应用启动时 Bean 装配失败 | 三个 profile 都补齐对应数据源配置 |
## 事务示例
```java
@Transactional(transactionManager = "transactionManagerPrimary")
public void saveMysqlData() {
// 调用 primary DAO
}
@Transactional(transactionManager = "transactionManagerSecondary")
public void saveOracleData() {
// 调用 secondary DAO;仅在确认允许写 Oracle 时使用
}
```