为什么写这篇:信创项目要求"一套代码能在 MySQL、PostgreSQL、达梦、人大金仓、H2 之间无缝切换"——但AUTO_INCREMENT vs SERIAL、LIMIT offset size vs LIMIT size OFFSET、` \ vs " 引号这些差异不抽象就硬编码就完了。本文给出一套经过生产验证的工程方案:MyBatis 方言 + JPA 改造 + 多数据源 + Flyway 差异化脚本。
适用读者:做国产化替代 / 多库支持的后端架构师。
前置知识:会用 Spring Boot + MyBatis/JPA、了解过 SQL 方言差异。
目录
- 核心原则:抽象共性,隔离差异
- MyBatis 多数据库适配方案
- JPA / Hibernate 多方言配置
- Spring 多数据源 Profile 隔离
- Flyway / Liquibase 差异化脚本管理
- 国产数据库特殊处理
- 测试与验证
1. 核心原则:抽象共性,隔离差异
1.1 三条铁律
铁律 1:不要硬编码数据库特定语法
1
2
3
4
5
6
7
8
9
| -- ❌ 错误(依赖 MySQL LIMIT 语法)
SELECT * FROM user LIMIT 0, 20
-- ❌ 错误(依赖 PG 语法)
SELECT * FROM user LIMIT 20 OFFSET 0
-- ✅ 正确:交给 ORM 转换
SELECT * FROM user
-- ORM 自动生成对应数据库的 LIMIT
|
铁律 2:不要硬编码数据类型
1
2
3
4
5
6
| -- ❌ MySQL 风格
VARCHAR(255)
-- ✅ 用 ORM 字段长度,让框架决定
@Column(length = 255)
private String name;
|
铁律 3:通过抽象层隔离
1
2
3
| 业务代码 → ORM(MyBatis / JPA)→ 驱动(JDBC)→ 数据库
↑ ↑
屏蔽方言 屏蔽连接差异
|
1.2 4 类必须隔离的差异
| 维度 | MySQL | PostgreSQL | 达梦 | 人大金仓 |
|---|
| 自增 | AUTO_INCREMENT | SERIAL / IDENTITY | IDENTITY | SERIAL |
| 分页 | LIMIT size OFFSET offset | LIMIT size OFFSET offset | LIMIT size OFFSET offset | 同 PG |
| 关键字引号 | ` | " | " | " |
| 字符串拼接 | CONCAT(a, b) | a || b | a || b | 同 PG |
| 布尔 | TINYINT(1) | BOOLEAN | BIT | BOOLEAN |
| 空串 | '' 算空 | '' 不算空 | '' 算空 | 同 PG |
| 事务隔离 | REPEATABLE READ | READ COMMITTED | READ COMMITTED | 同 PG |
2. MyBatis 多数据库适配方案
2.1 通用 Mapper / MyBatis-Plus(首选)
利用框架的 BaseMapper 自动适配:
1
2
3
4
| // MyBatis-Plus
public interface UserMapper extends BaseMapper<User> {
// 内置 selectById / insert / updateById / deleteById 自动适配所有数据库
}
|
2.2 差异化 SQL:_databaseId 动态判断
1
2
3
4
5
6
7
8
9
10
11
12
13
| <!-- UserMapper.xml -->
<select id="selectPage" parameterType="map" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name = #{name}</if>
</where>
<if test="_databaseId == 'mysql'">
LIMIT #{offset}, #{size}
</if>
<if test="_databaseId == 'postgresql' or _databaseId == 'kingbase' or _databaseId == 'dm'">
LIMIT #{size} OFFSET #{offset}
</if>
</select>
|
2.3 启用 _databaseId
1
2
3
4
5
6
7
8
9
10
| <!-- mybatis-config.xml -->
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="PostgreSQL" value="postgresql"/>
<property name="DM DBMS" value="dm"/> <!-- 达梦 -->
<property name="KingbaseES" value="kingbase"/> <!-- 人大金仓 -->
<property name="H2" value="h2"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
|
MyBatis 会自动调用 JDBC DatabaseMetaData.getDatabaseProductName(),匹配 value 注入到 _databaseId 变量。
2.4 数据库特定 SQL 完全隔离(备选方案)
1
2
3
4
5
6
7
8
9
| <!-- UserMapper_mysql.xml -->
<select id="selectPage" ...>
SELECT * FROM user LIMIT #{offset}, #{size}
</select>
<!-- UserMapper_postgresql.xml -->
<select id="selectPage" ...>
SELECT * FROM user LIMIT #{size} OFFSET #{offset}
</select>
|
用 namespace.id_mysql 命名空间隔离。不推荐——文件数量爆炸,维护成本高。
2.5 分页插件的统一抽象
PageHelper 5.x 已经支持自动识别数据库:
1
2
3
| PageHelper.startPage(1, 20);
List<User> list = userMapper.selectAll();
// 自动根据当前数据库生成 LIMIT
|
3. JPA / Hibernate 多方言配置
3.1 配置数据库方言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # application-mysql.yml
spring:
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
# application-postgres.yml
spring:
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
# application-kingbase.yml
spring:
jpa:
database-platform: org.hibernate.dialect.KingbaseESDialect
# application-dm.yml
spring:
jpa:
database-platform: org.hibernate.dialect.DmDialect
# application-h2.yml
spring:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
|
Hibernate 启动时会根据 database-platform 自动转换 JPQL → 对应方言。
3.2 复杂查询:原生 SQL + 方言判断
1
2
3
4
5
6
7
8
9
| @Query(value = "SELECT * FROM user WHERE id = :id", nativeQuery = true)
User findById(@Param("id") Long id);
// 多方言时
@Query(value = "SELECT * FROM user u " +
"WHERE (:schema = 'mysql' AND u.id = :id) " +
"OR (:schema != 'mysql' AND u.id = CAST(:id AS BIGINT))",
nativeQuery = true)
User findByIdMultiDb(@Param("id") String id, @Param("schema") String schema);
|
3.3 避免复杂 JPQL
反模式:把方言差异塞到 JPQL 里。
1
2
3
| // ❌ 错误:MySQL 特有 JSON 函数
@Query("SELECT u FROM User u WHERE JSON_EXTRACT(u.attrs, '$.role') = :role")
List<User> findByRole(@Param("role") String role);
|
正例:用 Specification(Criteria API)动态拼条件:
1
2
3
4
5
6
7
8
9
10
| public List<User> search(UserQuery query) {
return userRepository.findAll((root, cq, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (query.getName() != null) {
predicates.add(cb.equal(root.get("name"), query.getName()));
}
// 添加更多条件...
return cb.and(predicates.toArray(new Predicate[0]));
});
}
|
4. Spring 多数据源 Profile 隔离
4.1 配置文件分离
1
2
3
4
5
6
7
| src/main/resources/
├── application.yml # 主配置
├── application-mysql.yml # MySQL 配置
├── application-postgres.yml # PG 配置
├── application-kingbase.yml # 人大金仓
├── application-dm.yml # 达梦
└── application-h2.yml # 测试用 H2
|
4.2 主配置激活
1
2
3
4
5
| # application.yml
spring:
profiles:
active: mysql # 默认激活,可通过命令行覆盖
# 命令行:java -jar app.jar --spring.profiles.active=kingbase
|
4.3 各 profile 独立配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # application-mysql.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://internal.example.com:3306/industry_iot?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: {{REDACTED}}
# application-postgres.yml
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://internal.example.com:5432/industry_iot
username: postgres
password: {{REDACTED}}
# application-kingbase.yml
spring:
datasource:
driver-class-name: com.kingbase8.Driver
url: jdbc:kingbase8://internal.example.com:4321/industry_iot
username: kingbase
password: {{REDACTED}}
|
4.4 运行时动态切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Component
public class DataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CTX = new ThreadLocal<>();
public static void set(String ds) { CTX.set(ds); }
public static String get() { return CTX.get(); }
public static void clear() { CTX.remove(); }
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| # application.yml
spring:
datasource:
dynamic:
primary: mysql
datasource:
mysql:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://...
kingbase:
driver-class-name: com.kingbase8.Driver
url: jdbc:kingbase8://...
|
配合 dynamic-datasource-spring-boot-starter 实现 @DS("kingbase") 注解切换。
5. Flyway / Liquibase 差异化脚本管理
5.1 目录结构
1
2
3
4
5
6
7
8
9
10
11
12
| src/main/resources/
├── db/
│ ├── migration/
│ │ ├── mysql/
│ │ │ ├── V1__init.sql
│ │ │ └── V2__add_index.sql
│ │ ├── postgresql/
│ │ │ ├── V1__init.sql
│ │ │ └── V2__add_index.sql
│ │ └── kingbase/
│ │ ├── V1__init.sql
│ │ └── V2__add_index.sql
|
5.2 Flyway 命名约定
Flyway 自动按目录选择脚本——通过 flyway.locations 指定:
1
2
3
4
5
6
7
8
9
10
| # application-mysql.yml
spring:
flyway:
locations: classpath:db/migration/mysql
enabled: true
# application-postgres.yml
spring:
flyway:
locations: classpath:db/migration/postgresql
|
5.3 V1__init.sql 差异示例
MySQL:
1
2
3
4
5
| CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
PostgreSQL:
1
2
3
4
5
| CREATE TABLE user (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
|
人大金仓:
1
2
3
4
5
6
| CREATE TABLE user (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 跟 PG 几乎一致,KES 支持 PG 大部分语法
|
达梦:
1
2
3
4
5
| CREATE TABLE user (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
|
5.4 Liquibase 备选方案
Liquibase 用 YAML/XML 描述 schema,自动生成数据库特定的 DDL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # changelog.yaml
databaseChangeLog:
- changeSet:
id: 1
author: dev
changes:
- createTable:
tableName: user
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: name
type: VARCHAR(64)
constraints:
nullable: false
|
Liquibase 优势:一份 YAML,适配所有数据库(内置方言转换);劣势:复杂 DDL(视图、存储过程、触发器)支持差。
6. 国产数据库特殊处理
6.1 驱动依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| <!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- 达梦 -->
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>8.1.1.193</version>
</dependency>
<!-- 人大金仓 -->
<dependency>
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<version>8.6.0</version>
</dependency>
<!-- H2(测试用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
|
6.2 方言检查清单
| 库 | 官方支持 | 自定义 | 备注 |
|---|
| MySQL | ✓ | - | Hibernate 内置 |
| PostgreSQL | ✓ | - | Hibernate 内置 |
| 达梦 | 部分 | 需要 | Hibernate 5.6+ 引入 DmDialect |
| 人大金仓 | 部分 | 需要 | Hibernate 6.4+ 引入 KingbaseESDialect |
| H2 | ✓ | - | Hibernate 内置 |
| Oracle | ✓ | - | Hibernate 内置 |
| SQL Server | ✓ | - | Hibernate 内置 |
6.3 必做的 3 项测试
1
2
3
4
5
6
7
8
9
10
11
12
13
| -- 1. 主键自增验证
INSERT INTO user (name) VALUES ('test');
SELECT LAST_INSERT_ID(); -- MySQL
SELECT currval('user_id_seq'); -- PG
-- Hibernate 框架已经屏蔽
-- 2. 字符串拼接验证
-- MySQL: SELECT CONCAT('a', 'b')
-- PG: SELECT 'a' || 'b'
-- 3. 分页验证
-- MySQL: LIMIT 0, 20
-- PG: LIMIT 20 OFFSET 0
|
7. 测试与验证
7.1 用 @ActiveProfiles 切换测试环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| @SpringBootTest
@ActiveProfiles("mysql")
public class MysqlCompatibilityTest {
@Autowired private UserRepository userRepository;
@Test
public void testCRUD() {
User u = new User();
u.setName("test");
userRepository.save(u);
assertNotNull(u.getId());
assertEquals(1, userRepository.count());
}
}
@SpringBootTest
@ActiveProfiles("dm")
public class DmCompatibilityTest {
// 同样的测试
}
@SpringBootTest
@ActiveProfiles("kingbase")
public class KingbaseCompatibilityTest {
// 同样的测试
}
|
7.2 Testcontainers 拉真实数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Testcontainers
public class MultiDbIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test");
@Container
static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:14");
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
}
|
7.3 核心 4 项功能必须 100% 通过
| 功能 | 测试点 |
|---|
| CRUD | insert / select / update / delete |
| 分页 | LIMIT 在每种数据库都正确 |
| 事务 | 隔离级别、回滚、传播行为 |
| 特殊类型 | Date / Boolean / Enum / JSON |
8. 总结:方案对比
| 方案 | 复杂度 | 灵活度 | 适用 |
|---|
| MyBatis-Plus 通用 Mapper | 低 | 中 | 90% 业务 |
MyBatis _databaseId 方言 | 中 | 高 | 有 SQL 差异的复杂业务 |
| JPA + Hibernate 方言 | 中 | 中 | 标准 ORM 业务 |
| Spring 多数据源 + 动态路由 | 高 | 高 | 多租户 / 读写分离 |
| Flyway 差异化脚本 | 低 | 中 | DDL 同步 |
| Liquibase 自动转换 | 中 | 中 | 想少写 DDL 的项目 |
最佳组合:
1
2
3
4
5
6
7
8
9
| 业务层(Service)
↓
Repository(MyBatis-Plus 通用 Mapper)
↓
XML(_databaseId 方言处理少数特殊 SQL)
↓
Datasource(多数据源 Profile 隔离)
↓
Flyway(按库选择脚本)
|
经验总结
- 永远不要在 SQL 里直接写方言语法——交给 ORM 框架
- 分页、交集、字符串拼接这 3 类 SQL 是"方言重灾区"——必须用
_databaseId 隔离 - 测试必须覆盖所有目标数据库——尤其国产数据库的行为偶尔跟 PG 不一致
- Flyway 比 Liquibase 更适合差异化——后者用 YAML 太理想化,复杂 DDL 还是得手写
- 国产数据库的存储过程 80% 需要重写——别抱幻想,做不到 100% 自动转换
2024+ 视角:Spring Boot 3.3 + Hibernate 6.5 的方言新进展
Spring Boot 3.x 的多数据库自动推断
Spring Boot 3.0 起,只要引入驱动 + JPA starter,启动时自动推断方言——application.yml 里不必再手写 database-platform:
1
2
3
4
| # Spring Boot 3.0+ 不再需要
# spring:
# jpa:
# database-platform: org.hibernate.dialect.PostgreSQLDialect
|
只要 classpath 有 PG / MySQL / 达梦 / 人大金仓驱动,Hibernate 自动用对应方言。
达梦 DM 8 / 人大金仓 KES V9R2 新增官方方言
- Hibernate 6.4+:内置
KingbaseESDialect(之前需自定义) - Hibernate 6.5+:内置
DmDialect(达梦官方适配) - 旧项目自定义的
Dialect 扩展类可以删掉了
MyBatis-Plus 3.5.9+ 的多租户增强
TenantLineInnerInterceptor 支持多字段租户列、租户 + 业务多列组合- 配合
dynamic-datasource-spring-boot-starter 4.x,注解 @DS + @TenantLine 一起用不冲突
dynamic-datasource 4.x 关键变化
- 改用
spring.datasource.dynamic.datasource.<name> 命名空间(旧版 primary / datasource 平铺结构被废弃) - 内置 Druid + HikariCP 双连接池自适应——以前要二选一
seata 集成加了 xa-mode 开关,跨库分布式事务"开箱可用"
测试方案 2024+ 升级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Testcontainers 1.19+ 直接支持国产数据库镜像
@Testcontainers
public class KingbaseIntegrationTest {
@Container
static GenericContainer<?> kingbase = new GenericContainer<>("kingbase_v009r001c010b0004_single_x86:v1")
.withExposedPorts(54321)
.withEnv("DB_MODE", "pg");
@DynamicPropertySource
static void setup(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", () ->
"jdbc:kingbase8://" + kingbase.getHost() + ":" + kingbase.getMappedPort(54321) + "/test");
}
}
|
AI 时代的 SQL 方言"自动翻译"
- SQLGlot(Python):纯 Python 实现的 SQL 方言转换器,覆盖 20+ 方言——可在 CI 流水线里加一道"SQL 方言一致性检查"
- Vanna.ai / Chat2DB:用 LLM 把自然语言转 SQL 并自动选方言,但生产慎用(不保证 100% 正确)
- 传统 ORM 抽象仍是工程首选——AI 生成 SQL 仅供 PoC
信创 + AI 时代的"两库"演进
| 维度 | 2020 | 2024+ |
|---|
| 国产数据库 | 达梦 / 人大金仓 / 神通 | OceanBase 4.x / TiDB 7.x / openGauss 6.x 进入主流视野 |
| 选型标准 | PG 兼容 + 价格 | 兼容 + 性能 + 分布式 + 工具链 |
| 适配成本 | 60% 项目有适配层 | OceanBase / openGauss 借 PG 协议可"零代码"切换 |
| AI 嵌入 | 无 | 向量检索(OceanBase 4.3+、TiDB 8+ 内置 vector type) |