Featured image of post 多数据库适配架构:MyBatis 方言 + JPA + Flyway 全套方案

多数据库适配架构:MyBatis 方言 + JPA + Flyway 全套方案

本文系统整理一套代码兼容 MySQL/PostgreSQL/达梦/人大金仓/H2 等多数据库的工程实践:MyBatis `_databaseId` 方言切换、JPA `databaseIdProvider` 改造、多数据源 Profile 隔离、Flyway 差异化脚本、国产数据库驱动与方言配置。

为什么写这篇:信创项目要求"一套代码能在 MySQL、PostgreSQL、达梦、人大金仓、H2 之间无缝切换"——但AUTO_INCREMENT vs SERIAL、LIMIT offset size vs LIMIT size OFFSET、` \ vs " 引号这些差异不抽象就硬编码就完了。本文给出一套经过生产验证的工程方案:MyBatis 方言 + JPA 改造 + 多数据源 + Flyway 差异化脚本。

适用读者:做国产化替代 / 多库支持的后端架构师。

前置知识:会用 Spring Boot + MyBatis/JPA、了解过 SQL 方言差异。

目录

  1. 核心原则:抽象共性,隔离差异
  2. MyBatis 多数据库适配方案
  3. JPA / Hibernate 多方言配置
  4. Spring 多数据源 Profile 隔离
  5. Flyway / Liquibase 差异化脚本管理
  6. 国产数据库特殊处理
  7. 测试与验证

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 类必须隔离的差异

维度MySQLPostgreSQL达梦人大金仓
自增AUTO_INCREMENTSERIAL / IDENTITYIDENTITYSERIAL
分页LIMIT size OFFSET offsetLIMIT size OFFSET offsetLIMIT size OFFSET offset同 PG
关键字引号`"""
字符串拼接CONCAT(a, b)a || ba || b同 PG
布尔TINYINT(1)BOOLEANBITBOOLEAN
空串'' 算空'' 不算空'' 算空同 PG
事务隔离REPEATABLE READREAD COMMITTEDREAD 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% 通过

功能测试点
CRUDinsert / select / update / delete
分页LIMIT 在每种数据库都正确
事务隔离级别、回滚、传播行为
特殊类型Date / Boolean / Enum / JSON

8. 总结:方案对比

方案复杂度灵活度适用
MyBatis-Plus 通用 Mapper90% 业务
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(按库选择脚本)

经验总结

  1. 永远不要在 SQL 里直接写方言语法——交给 ORM 框架
  2. 分页、交集、字符串拼接这 3 类 SQL 是"方言重灾区"——必须用 _databaseId 隔离
  3. 测试必须覆盖所有目标数据库——尤其国产数据库的行为偶尔跟 PG 不一致
  4. Flyway 比 Liquibase 更适合差异化——后者用 YAML 太理想化,复杂 DDL 还是得手写
  5. 国产数据库的存储过程 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 时代的"两库"演进

维度20202024+
国产数据库达梦 / 人大金仓 / 神通OceanBase 4.x / TiDB 7.x / openGauss 6.x 进入主流视野
选型标准PG 兼容 + 价格兼容 + 性能 + 分布式 + 工具链
适配成本60% 项目有适配层OceanBase / openGauss 借 PG 协议可"零代码"切换
AI 嵌入向量检索(OceanBase 4.3+、TiDB 8+ 内置 vector type)

使用 Hugo 构建
主题 StackJimmy 设计