# 双数据源说明 本服务使用两套手工配置的 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 Entity,Secondary EMF 只认识 Secondary Entity。也就是说,一个 DAO 所操作的 Entity 必须被**同一套 EMF**管理。 > **一句话记忆:DAO 在哪个 `*.dao` 根包,就由该包对应的 Config 创建代理;该 Config 指向哪一个 EMF / DataSource,DAO 就访问哪一个数据库。** 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 时使用 } ```