异构数据库迁移

以 Oracle 19c 迁移 MySQL 8.0 为例,寻求一种心智负担最小的可行方案。
阅前须知:
- 本文只考虑 Oracle、MySQL、PostgreSQL 三种比较知名的关系型数据库管理系统。
- 本文不考虑依托于庞大云服务的云数据库,云服务供应商应该会提供更强大的方案。
- 对于视图、物化视图、触发器、存储过程以及其他高级特性的异构数据库迁移不在本文能够处理的范围内。
- 本文的视角以 Oracle 往 MySQL 的迁移为主,涉及到 Oracle 往 PostgreSQL 的迁移,可能有其他更好的方案。
- 由于授权问题,商业版工具不在本文考虑范围内。
- 未经过实际生产验证,本文所有方案不适用于线上生产环境。
- 若无特别说明,本文提到的“表结构”主要包含字段类型、索引、约束等信息。
- 本文虽然明面考虑的是异构数据库迁移,但实际上更倾向于考虑长期同时支持多个异构数据库运作。
- 本文提到的大部分框架,本人没有深度使用经验,在实际应用中使用这些框架可能会带来一些挑战。
Oracle 快速搭建
使用 docker 搭建 oracle
1 | docker pull registry.cn-hangzhou.aliyuncs.com/zhuyijun/oracle:19c |
运行容器
1 | docker run -itd --name oracle19c \ |
Oracle 属于重量级数据库,docker 启动可能需要等待一定时间
这个镜像可能存在 initorcl.ora 文件缺失的问题,可以从 pfile 下拷贝 init.ora.xxxx 文件至 dbs 目录下重命名为 initorcl.ora 文件,并设置为 644 权限。
1 | docker exec -it oracle19c bash |
MySQL 快速搭建
使用以下 docker-compose.yaml 快速搭建一个 MySQL 实例
1 | services: |
1 | docker compose up -d |
客户端连接
Oracle 自带的 sqlplus 工具使用体验较差,为了方便,可以使用 Navicat premium lite 17.3.4 版本(初次使用需要注册登录)或者考虑使用 Oracle SQLcl(Oracle 官方推出的相对于 sqlplus 的现代化命令行工具)。
Oracle 连接配置用户名为SYSTEM、密码为 docker 指定的ORACLE_PWD
参数值、服务名称为 orclPDB1。
在 Navicat 中需要依次新建表空间(相当于MySQL中的数据库)、新建用户并关联表空间、授权,然后再以新用户的身份连接,才能使用其关联的表空间。
如果使用 sqlcl,可参考以下命令
1 | # 连接到 PDB |
进入 SQL Shell 内,创建表空间、用户和关联用户表空间、授权连接权限
1 | CREATE TABLESPACE apps |
切换连接
1 | conn appuser/Secure123@172.17.0.1:1521/orclPDB1 |
准备工作
本人并不是 Oracle 的深度用户,本着“以核心高频使用的数据类型为主,剔除过时/不推荐的数据类型,保留必要的边界测试”的理念来设计测试表,同时考虑不规范设计导致的隐式行为对迁移工作带来的问题。
表结构定义
以下是基于 LLM 生成的测试用例,不代表真实业务
核心业务数据类型测试表
1 | -- 核心业务数据类型测试表 (高频场景) |
边界与规范陷阱测试表
1 | -- 边界与规范陷阱测试表 (迁移关键验证点) |
高频不规范类型测试表
1 | -- 高频不规范类型测试表 (真实业务常见但危险的写法) |
测试数据生成
具体的测试数据可以使用以下 LLM 提供的 SQL 脚本自动生成
CORE_DATA_TEST
1 | -- 生成 CORE_DATA_TEST 表测试数据 |
CORE_DATA_TEST 表数据特点:
- 数值类型:
- 金融价格包含边界值(0.0001和99999.9999)
- 科学评分包含NaN和Infinity特殊值
- 字符串类型:
- 用户名使用字符语义(VARCHAR2 CHAR)
- 描述包含多语言字符(中文、俄文、日文)
- JSON数据验证JSON约束
- 日期时间:
- 创建时间使用微秒精度TIMESTAMP(6)
- 过期时间带时区信息
- 二进制数据:
- 小头像使用小尺寸BLOB
- 元数据使用固定长度RAW(200)
EDGE_CASE_TEST
1 | -- 生成 EDGE_CASE_TEST 表测试数据 |
EDGE_CASE_TEST 表边界测试:
数值边界:
- 最大38位整数(10³⁸-1)
- 浮点NaN和Infinity特殊值
字符编码陷阱:
字节语义字符串包含多字节字符(测试截断)
Emoji字符串混合文本和表情符号
时间边界:
- 最小日期(0001-01-01)
- 最大时间戳(9999-12-31 23:59:59.999999999)
空值/大对象:
- 空CLOB(EMPTY_CLOB()非NULL)
- 显式NULL二进制数据
ANTIPATTERN_TEST
1 | -- 生成 ANTIPATTERN_TEST 表测试数据 |
ANTIPATTERN_TEST 表数据说明:
- 体现数值型不规范:
price
:生成带小数的随机值,展示无精度控制的问题discount
:生成0-1之间的随机浮点数,展示精度问题quantity
:生成整数,但使用变长存储tax_rate
:生成整数税率,但未指定小数位
- 体现字符串型不规范:
name
:生成长度1-4000的随机字符串(滥用最大长度)address
:生成长度1-2000的Unicode字符串(性能问题)remark
:90%为短字符串,10%为4000字符超长字符串(索引问题)
- 体现日期型不规范:
created_date
:生成带时间分量的日期(业务只需日期)updated_time
:生成高精度时间戳(默认6位小数)
- 体现隐式转换陷阱:
numeric_string
:存储格式化的数字字符串date_string
:存储格式化的日期字符串
- 数据多样性:
- 使用
DBMS_RANDOM
包生成随机数据 - 包含边界值(如超长字符串)
- 生成1000行代表性数据
- 使用
迁移方案
迁移步骤分成两个阶段,结构迁移和数据迁移。结构迁移的主要问题是异构数据库数据类型的兼容。由于 SQL 标准并不负责规定各种数据库的具体类型定义,Oracle、MySQL 等的数据类型并不一致,不过 Oracle 中实际高频使用的类型基本在 MySQL 中有等价或近似的类型,但即便有一定程度的兼容性,也仍有可能存在一些细节问题,甚至还得考虑重新设计源数据库数据类型,以满足兼容性。
废案历史
主要讨论结构迁移部分。
第一版废案:liquibase(community) + 脚本处理,得到 MySQL DDL。liquibase(community) 生成的 changelog 无法解决 Oracle 与 MySQL 之间的 DDL 差异性问题。DDL 相差较大,脚本处理可靠性存疑且需要耗费精力验证。
考虑 liquibase(community) 而非 flyway,主要原因是 diff-changelog 的功能,flyway 的类似 diff 功能仅在商业版提供。但在能够获取到 DDL 的情况下,两者都能很好地实现 SQL 版本控制。
此外,另一个轻量工具 sqldef/sqldef 能够导出数据库 DDL,并能将修改过后的 DDL 与当前数据库进行对比生成差异的 DDL,但仅限于同构的数据库,且不支持 Oracle。
第二版废案:AWS SCT。AWS SCT 能够对比异构数据库差异并提供建议,但是还是需要人工干预处理,且 AWS SCT 主要为 AWS 的云服务工作,对本地自建数据库支持较弱。
第三版废案:ora2pg + PostgreSQL,利用 PostgreSQL 作为中间者,间接实现 Oracle 到 MySQL 的迁移。但是实际上围绕 PostgreSQL 的工具基本都是单向迁移工具,且只能往 PostgreSQL 迁移。推测 PostgreSQL 相关的工具之所以只能实现单向迁移,是因为 PostgreSQL 本身有足够大的能力可以“容纳另一个数据库的所有常用功能”,反之则不成立。进行迁移的过程中,底层实现的不同导致“元信息”发生不可逆的转变,因此也不可能再进行反向迁移了。
目前方案
经过漫长的考虑,这里最终决定采用一种特别的方案:利用具备自动建表功能(或者是能够自动生成 DDL 方言)的框架基于实体类、结构体代码来实现对 MySQL 的自动建表。
在该方案的上一版中,其实还有一个步骤就是基于 Oracle 的已有表结构生成实体类、结构体代码。但是综合多种因素考虑,该步骤可能并不是必要的。
基于 Oracle 表结构提取并生成代码这一点,Java 可能是最省力的方案(然而实际上它所生成的实体类缺少必要的注解信息,无法与支持自动建表的框架集成),而不少出现在这之后的新语言例如 Go、Rust 等可能都无法很好地与 Oracle 适配。但这一点也并非是硬性要求,详情见下文说明。
另一方面,这些新语言也大概率不会选择 Oracle 作为开发环境,而是选择更开放的 PostgreSQL 等,因此也就不存在与 Oracle 相关的迁移需求。
自动建表框架选型:
Java
babyfish-ct/jimmer (未来有计划支持,截止目前 2025.10 不支持)
有相关计划支持 DDL 生成,不过目前仍在规划中
Feature request: SQL DDL generation · Issue #286 · babyfish-ct/jimmer
但可能不会考虑直接在代码运行的过程中改动数据表结构
是否可以支持将表关系定义持久化到数据库里? · Issue #3 · babyfish-ct/jimmer
相关讨论表示未来某个版本可能会上线 DDL 生成功能
关于 DDL 的一些想法。Some thoughts on DDL. · babyfish-ct/jimmer · Discussion #996
dromara/auto-table (dromara 开源社区孵化项目)
支持在没有 JPA 环境的项目中也能实现自动维护表结构,并提供了对 mybatis-plus 的支持。
dromara/mybatis-plus-ext: mybatis-plus框架的拓展包,在框架原有基础上做了进一步的轻度封装,增强内容:多数据源自动建表、数据自动填充、自动关联查询、冗余数据自动更新、动态查询条件等。
hibernate/hibernate-orm (传统 JPA 规范实现框架)
可能是最早支持自动维护表结构的框架,也是 spring-projects/spring-data-jpa 的默认底层实现。但是依赖 JPA 环境,在已经使用 Mybatis 相关框架的项目中还需要额外配置 JPA。
Golang
原生支持自动表结构维护的 orm 框架。
Simple and Powerful ORM for Go, support mysql,postgres,tidb,sqlite3,sqlite,mssql,oracle,cockroach.
这个似乎实验性支持 Oracle 数据库。
可能的伪需求:基于已有的表结构生成实体类、结构体代码
在项目生命周期里,结构体或实体类总是与数据表同步进行的,通常也不会出现“数据表一万张,实体类或结构体一个没写”的情况(除非是要将项目代码和数据库一并更换,那这也就不在本文的考虑范围内了)。
在实体类、结构体代码都已经齐全的情况下,大部分语言可以利用各自的框架来实现对 MySQL 的自动建表。因此,直接基于当前项目所使用的语言来进行自动建表即可,并不存在从 Oracle 中提取表结构来生成代码的需求。
此外需要注意判断框架能否支持在已有表的基础上通过修改实体类、结构体代码来实现对数据表结构的新增、删除和修改等,以决定框架的使用时机,否则会造成代码与数据表的不一致。
但是出于某种原因考虑,本人还是决定使用 mybatis-generator 尝试生成 Java 实体类代码。
快速新建一个 maven 项目
1 | mvn archetype:generate \ |
pom.xml 补充内容
1 | <properties> |
src/main/resources/generatorConfig.xml 配置
1 |
|
生成代码
1 | mvn mybatis-generator:generate |
运行过程中发现了以下日志
1 | [WARNING] Unsupported Data Type 101 in table APPUSER.CORE_DATA_TEST, column: RATING, property defaults to Object type. |
Oracle 中的 BINARY_FLOAT、BINARY_DOUBLE、TIMESTAMP WITH TIME ZONE 属于非标准类型,没有在 mybatis-generator 中包含,所以被默认处理为了 Object 类型,但实际上这些类型可以对应到 Java 中更具体的类型。通过拓展 mybatis-generator 的 resolver 可以自定义实现对这些类型的处理。
另外新建一个项目
1 | mvn archetype:generate \ |
修改 pom.xml
1 | <properties> |
自定义拓展 mybatis-generator 的 JavaTypeResolverDefaultImpl
1 | package com.example.extension; |
安装到本地仓库
1 | mvn clean install -DskipTests |
在原来的项目 pom.xml 中补充依赖
1 | <dependency> |
在 src/main/resources/generatorConfig.xml 中加入自定义映射配置。注意配置有顺序要求,需要放在 jdbcConnection
与 javaModelGenerator
之间
1 | <!-- 自定义映射配置 --> |
重新生成代码
1 | mvn mybatis-generator:generate |
有一点值得考虑的是,当代码生成器无法准确地判断数据字段类型到底对应哪种代码内置类型时,是否能够认为该数据字段类型是一个数据库专有类型或者是一个不被推荐使用的类型?
结构迁移
本文采用 mybatis-plus-ext(auto-table 扩展) 的方案来实现 Oracle 往 MySQL 的表结构迁移。
初始化项目
1 | spring init -d=mysql,lombok --build=maven demo |
补充依赖
1 | <dependency> |
application.yaml
1 | spring: |
application-dev.yaml
1 | spring: |
启动类上增加 @EnableAutoTable 注解
1 |
|
CoreDataTest 实体类示例,更多用法参考 Dromara AutoTable | Auto Table。
1 |
|
CoreDataTest、EdgeCaseTest 根据前面的 DDL 可以提取出较多的数据表元信息,而 AntipatternTest 只能依靠框架的隐式行为或者人工手动规范。实际上 AntipatternTest 在前面的代码生成器中就已经生成了错误的类型,基本可以认定为无法直接用于迁移,是不能在数据表设计中出现的反规范设计。
数据迁移
解决了前面的类型兼容问题,数据迁移相对就没有那么麻烦。数据可以通过直接导出平台无关的通用数据格式文件,例如 csv 文件,再导入到异构数据库中。这对于小规模数据迁移很方便,对于大规模数据这里采用的是 apache/seatunnel。
顺便一提,apache/seatunnel 也具备自动建表的功能,但是需要依赖已存在的、规范的数据源,并且无法完整迁移索引等结构。
拉取 seatunnel 镜像,seatunnel 的镜像比较大,拉取需要等待一定时间
1 | docker pull apache/seatunnel |
编写配置文件 o2m.config,更多配置可查询 官方文档
1 | env { |
运行服务
1 | docker run --rm -it -v ./:/config apache/seatunnel ./bin/seatunnel.sh -m local -c /config/o2m.config |
不出所料,ANTIPATTERN_TEST 因为元信息的规范性问题,迁移的数据出现了因类型错误导致数据存储格式不正确的问题,比较明显的就是 NUMBER 类型没有明确指定精度,导致主键 ID 的类型变成了 DECIMAL(38, 18)。
而 CORE_DATA_TEST 和 EDGE_CASE_TEST 则出现了部分数据无法迁移的问题。以这里的测试数据为例,经过排查发现是因为 TIMESTAMP 类型的一个极端值超过了 MySQL 中 timestamp 类型的上限,以及 BINARY_FLOAT 和 BINARY_DOUBLE 类型的测试数据中包含了 nan 和 inf 这两个特殊值。
这两个特殊值在 MySQL 中没有相应的支持,实际应用中出于兼容目的需要考虑使用其他方案替代。
在除去了极端值以及 nan 和 inf 这两个特殊值后,CORE_DATA_TEST 和 EDGE_CASE_TEST 也顺利地完成了迁移。
一些思考
个人观点
一个开发哲学问题:先写代码还是先写 DDL ?
传统的项目开发中,DDL 总是必不可少的存在,项目起步通常是先在数据库执行 DDL,有了基础的数据库环境后,再着手于项目代码的编写,然后随着新功能的开发就是反复地执行 DDL、编写代码。然而这可能会存在以下问题:
- 如果要同时支持多个异构数据库,设计成本巨大,且难以保证兼容性。
- 不利于处于需求频繁变化时期的项目的快速迭代。
- 需要人为去把控数据表设计的规范性,或者需要借助专门的审查平台。
- 后期改动数据表成本巨大。
- 容易将一部分本该由代码控制的业务逻辑泄露到数据库中 (定义了一系列与业务关联的触发器、存储过程等,会为开发带来巨大的心智负担,而且不利于异构数据库的兼容)。
不过 DDL 优先设计仍有其存在的必要性,大部分时候可能还是最符合习惯与直觉的开发流程,只是需要基于一些影响因素来考虑使用。
明确项目未来只会支持单一数据库类型,如果未来必须要支持多个异构数据库,则需要考虑牺牲一定的性能来换取兼容性。
只是为了某一个数据库特有的性能优化,而放弃与其他数据库的兼容性,是否是一个战略上正确的决定?
如果有足够强大的资源能够同时支持多个异构数据库,并能为每个异构数据库做专门优化的,可以忽略这一点。
明确是不需要快速迭代的大型项目,或者拥有一套强大的多 DDL 方言版本管理方案。
团队整体应该指定并遵循严格的数据表设计规范,不能出现不清晰的设计。可以借助 SQL 审查平台基于规则自动检查设计的规范性,同时保存改动记录,便于随时回滚。
有的团队中可能会存在 DBA 或者其他由 DBA 转变而来的现代化新角色来处理数据库相关的工作,这与“每个团队成员需要具有一定数据表设计规范意识”并不冲突。
小规模或个人项目,数据库变动不复杂、规模小、且成本完全可控。
这种情况下,其实也更推荐去尝试新的开发方式。
现代项目的开发中,已经出现打破了这种 DDL 优先设计的技术。比较知名的就是以 golang 语言主导的项目中的 gorm 框架,能够通过定义结构体来实现对数据表结构的创建。
不过打破 DDL 优先设计并不是说要脱离 DDL。出于审计、可追溯或者版本控制的目的,DDL 仍会在项目开发中出现,只是说不再需要人为去设计编写,而是由框架自动生成。
其实在更早的时候就已经出现了通过代码来自动生成数据表结构的技术(这里简称为 code first),典型的例子就是 hibernate,具有这种思维的各种技术后来被统一为 JPA 规范。
JPA 规范的核心目标:提供一套统一、标准化的API,将开发者从繁琐的 JDBC 和 SQL 细节中解放出来,实现用面向对象的方式操作数据库。
JPA 的设计理念是没问题的,并且契合了领域驱动设计的思想,但是可能过于理想化 (没有银弹)。JPA 规范的实现者们可能并没有真正理解 JPA 的目标,JPA 的使用者们可能也并没有学会如何正确地驾驭这套体系。
实际上后来的 mybatis-plus 也在通过插件增强,以获得与 JPA 类似的便利性。历史上有不少个人或社区孵化项目尝试将 JPA 的 code first 理念应用到 mybatis-plus 的框架上
- YoWenhow/mybatis-enhance-actable(可能已停止维护)
- mybatis-plus-auto-ddl(可能已停止维护)
- auto-ddl(可能已停止维护)
- mybatis-enhance-actable(可能已停止维护)
- dromara/auto-table(活跃更新)
也有将 JPA 与 Mybtais/Mybtais 配合使用的方式,集两者之长
从现在的视角来看,不妨可以尝试混合持久化:采用“80/20 原则”。即80%简单、标准的 CRUD 操作使用 JPA,提升开发效率;而对 20% 性能关键的复杂查询、报表或批量操作,使用原生 SQL 或更灵活的轻量级工具。或者说 mybatis-plus 本身就在实现这样一种思想。
JPA 规范的实现与实际应用也许存在诸多问题(阻抗失衡、N+1 问题),但其 code first 的设计理念始终影响着后继者,并且与现在的 IaC 理念有着异曲同工之妙。
未来的范式:代码定义结构?
这里需要考虑两种情况:
A:在原有项目的基础上引入
B:在全新的项目里引入
前面提到的 JPA、gorm 其实还是绑定于各自的生态,如果已有项目并没有使用这些框架,就需要花费额外的成本去引入,侵入性较大。
有一个值得关注的项目 prisma/prisma,号称是适用于 Node.js 和 TypeScript 的下一代 orm 框架,统一通过 prisma schema 来进行数据建模,不需要分别在关系型数据库层面和应用程序层面进行数据建模。配合社区客户端实现,能支持生成 go、rust、python 等语言的代码。
但是在 2025 年初,官网打算将未来的 prisma v7 进行重写,这导致一大片客户端决定停止维护。
- Deprecation of the Prisma Go client · Issue #1542 · steebchen/prisma-client-go
- PCR is no longer being maintained · Brendonovich/prisma-client-rust · Discussion #476
- Deprecation of the Python client · Issue #1073 · RobertCraigie/prisma-client-py
虽然理论上可以通过开发新的客户端来适配 prisma v7,但这对社区无偿贡献的开发者来说无疑是一个很大的负担。并且正如 prisma 官方博客中所说的:“支持多个客户端的能力,不再是我们的重点”。
原文摘录:
Additionally, the core benefit of the query engine—the ability to support multiple clients—is no longer our focus. Prisma ORM is a TypeScript project and while we support our community clients, we won’t be developing them in house.
prisma 官方的博客文章地址:From Rust to TypeScript: A New Chapter for Prisma ORM
基于前面的思考,一种理想化的代码定义结构的基础设施可能是:
输入唯一性:仅允许实体类/结构体代码作为来源。
实体类或结构体代码总是能与数据表结构相对应,这能从框架层面上反映项目代码实际需要什么样的表结构来支持运行。而人工手动维护的 DDL,尤其还需要维护多份异构数据库的方言,这个过程本身就很耗费精力,很容易产生人为失误。因此选择代码作为来源是最可靠的依据。(当然代码也会存在失误,但相比数据表层面,代码上的结构性失误可能更容易被发现)
同时这也变相地要求数据表的唯一来源是代码,以及要求代码具备能够控制一些数据库高级特性的能力。
前面提到的 prisma schema 可能也有潜力同时作为实体类/结构体代码和数据表的唯一来源,但是过于依赖社区实现。
零侵入性(?):不需要改变已有代码。
主要是针对情况 A 考虑。
这点其实存在悖论,因为现有的代码定义结构的方案中,都是通过修改代码以包含索引、约束等元信息。如果不对代码做修改,那么就无法实现代码定义结构,如果要实现代码定义结构,那就必须得修改已有代码。
按照前面的说法,数据表的唯一来源是代码,那么一开始数据表是不存在的,元信息自然也就不存在(不考虑隐式行为),代码不包含元信息且不能改动,那就必然无法实现元信息的创建,因为这个过程中元信息根本就不存在,是一个不可能三角。
零学习成本或低使用成本(?):无需学习新的 DSL 语法或者 DSL 能够同时处理编程语言代码和数据表两个层面,提高开发效率。
针对情况 A,新的 DSL 如果还是以人工手动编写的方式运作,那么它和人工维护 SQL 方言没有本质区别。
针对情况 B,考虑到 DSL 并不仅限于对数据表的控制,如果也能控制代码层面,避免手动编写大量的异构编程语言后端代码 (需要同时进行后端、数据库迁移的,并需要长期支持原系统和新系统),也许是另一种可能。
全生命周期支持:空库创建→增量变更→表管理
数据表结构不仅能通过代码自动创建,还能基于代码的变动来对比差异并自动调整表结构以适应代码。以此实现代码全面管理数据表。更进一步还能支持分布式集群的同构、异构数据库管理。
多方言自动化:消除 SQL 方言差异
最核心的功能,基于目标数据库的类型自动处理表结构,解决人工处理异构数据库方言差异的问题。
不过上面的想法还是过于理想化。它们在实际应用中能否得以实现以及在实现过程中会有什么困难,还是无法确定的事情。
参考资料
Docker 安装 Oracle在Docker中快速使用各个版本的Oracle数据库(10g、11g、12c、18c、1 - 掘金
工欲善其事,必先利其器,用上AWS SCT迁移工具,其它的都是将就 - 墨天轮
Oracle迁移PostgreSQL方案 - Cody& - 博客园
Springboot+mybatis-plus+ACTable实现自动创建表结构 - oldwu’s Blog
Introduction to MyBatis Generator – MyBatis Generator Core
为什么你的MyBatis总产生N+1问题?原理与根治方案 - 今日头条
01 发展历程:IaC过去、现在与未来展望 - 极客时间文档
Prisma 的全部命令和 schema 语法本文详细介绍了 Prisma 的全部命令,包括初始化项目、配置数据库、生成 - 掘金
大家在工作中创建服务端项目的数据库表都是怎么设计怎么建的?手写 SQL,用 GUI 工具还是 ORM 框架? - V2EX
- 标题: 异构数据库迁移
- 作者: Entropy Tree
- 创建于 : 2025-10-19 10:23:53
- 更新于 : 2025-10-19 10:59:08
- 链接: https://www.entropy-tree.top/2025/10/19/heterogeneous-database-migration/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。