
简介这是一款基于Java开发的数据库脚本转换工具面向需要进行MySQL到Oracle迁移的开发者与数据库管理员用于解决两种数据库在语法、数据类型、函数及存储过程上的兼容差异降低手工改写脚本的时间与出错风险。资源包共34个文件约3.01MB以java源码与class编译文件为主体辅以6个jar依赖库、properties配置、md说明及工程配置文件涵盖SQL解析、语法映射、数据库连接与脚本生成等模块结构完整便于二次开发与调试。目前已有183人学习下载。通过阅读源码读者可掌握MySQL的DDL与DML语句解析、Oracle兼容格式转换、视图与触发器特殊处理等实现思路并借助commons-dbutils、ojdbc、mysql-connector等依赖理解数据库操作与文件I/O流程适合作为数据库迁移、SQL解析及Java工程实践的学习参考。1. 从 MySQL 到 Oracle 的脚本转换为什么手工改 DDL 迟早要翻车同一个系统从 MySQL 迁到 Oracle最容易被低估的环节不是数据搬运而是建表脚本的转换。我见过太多团队一开始觉得“不就是改改字段类型”结果在AUTO_INCREMENT、反引号、TINYINT、DATETIME这些地方反复翻车最后靠人肉逐行改改到第三十个表就开始怀疑人生。基于 Java 的数据库脚本转换工具要解决的正是这件事把 MySQL 的 DDL 脚本批量、可复现地转成 Oracle 能直接执行的脚本而不是靠 DBA 的肌肉记忆。这类工具适合三类人正在做异构数据库迁移的后端工程师、需要交付 Oracle 版本的项目组、以及想把转换规则沉淀成代码而不是文档的团队。它的核心价值不在“转一次”而在“规则可维护、结果可校验、批量可重跑”。下面我按自己实际做过的路径把选型、解析、映射、避坑和验证一层层拆开讲清楚。2. 转换工具的技术选型为什么用 Java 而不是正则一把梭2.1 正则替换为什么在真实脚本上撑不过三个表很多人第一反应是写一堆String.replace把INT换成NUMBER、把反引号删掉。小脚本能跑但真实项目里 DDL 有注释、有ENGINEInnoDB、有COMMENT、有联合索引、有DEFAULT CURRENT_TIMESTAMP正则一旦遇到嵌套括号或字符串里的逗号就会错位。更麻烦的是正则无法区分“字段名”和“关键字”比如一个字段叫number你替换类型时可能把它一起改了。所以工具的第一层能力必须是结构化解析而不是文本替换。常见做法是用 ANTLR 生成 MySQL 语法解析器或者用现成的 SQL 解析库如 JSqlParser、Druid 的 SQL Parser。我一般会选 Druid 的SQLUtils配合自定义 AST 访问器因为它对 MySQL 方言支持比较全社区踩坑记录也多。2.2 用 Java 搭一个最小可跑的解析骨架下面这段代码演示如何用 Druid 解析一条 MySQL 建表语句并遍历字段定义。它是整个转换器的起点后面所有类型映射都挂在这个 AST 上。import com.alibaba.druid.sql.SQLUtils; import com.alibaba.druid.sql.ast.SQLStatement; import com.alibaba.druid.sql.ast.statement.SQLCreateTableStatement; import com.alibaba.druid.sql.ast.statement.SQLColumnDefinition; import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor; import java.util.List; public class DdlParser { public static void main(String[] args) { String mysqlDdl CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(64) DEFAULT NULL COMMENT 用户名, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;; // 解析为 AST指定 MySQL 方言 ListSQLStatement statements SQLUtils.parseStatements(mysqlDdl, mysql); for (SQLStatement stmt : statements) { if (stmt instanceof SQLCreateTableStatement) { SQLCreateTableStatement createTable (SQLCreateTableStatement) stmt; System.out.println(表名: createTable.getName().getSimpleName()); for (SQLColumnDefinition column : createTable.getColumnDefinitions()) { System.out.println(字段: column.getName().getSimpleName() | 类型: column.getDataType() | 是否自增: column.isAutoIncrement()); } } } } }逻辑说明SQLUtils.parseStatements把文本变成 AST第二个参数mysql决定方言。SQLCreateTableStatement提供getColumnDefinitions()每个SQLColumnDefinition能拿到字段名、数据类型、是否自增、默认值、注释。参数上要注意Druid 版本不同isAutoIncrement()的可用性有差异建议锁定 1.2.x 以上如果脚本里有 Oracle 不支持的ENGINE、CHARSET这些在 AST 里属于表选项需要单独过滤。2.3 类型映射表怎么定别照搬网上那张图类型映射是转换器的灵魂。网上流传的对照表往往只写INT - NUMBER但真实场景要区分INT、BIGINT、TINYINT、INT UNSIGNED。我一般用一张可配置的映射表而不是硬编码在代码里方便不同项目微调。MySQL 类型Oracle 类型备注TINYINTNUMBER(3)布尔语义时可用 NUMBER(1)INT / INTEGERNUMBER(10)无符号需扩到 NUMBER(10) 以上BIGINTNUMBER(19)对应 64 位VARCHAR(n)VARCHAR2(n CHAR)注意 Oracle 默认字节语义TEXTCLOB大文本必须换 CLOBDATETIME / TIMESTAMPTIMESTAMP精度按需加 (6)DECIMAL(p,s)NUMBER(p,s)直接对应DOUBLEBINARY_DOUBLE别用 FLOAT精度会丢这张表要放在配置文件里比如type-mapping.yml代码读取后按字段类型查表。这样遇到特殊项目改配置不用重新编译。3. 把 MySQL DDL 转成 Oracle 脚本从 AST 到可执行 SQL 的完整链路3.1 自增主键怎么转序列加触发器的两种写法MySQL 的AUTO_INCREMENT在 Oracle 里没有直接对应。常见做法有两种一是建序列加触发器二是用IDENTITY列Oracle 12c 及以上支持。如果目标库是 11g只能用序列加触发器。下面是用 Java 生成序列和触发器的代码片段。public class AutoIncrementConverter { // 为每个自增字段生成序列 触发器 public static String convert(String tableName, String columnName) { String seqName SEQ_ tableName.toUpperCase(); String triggerName TRG_ tableName.toUpperCase(); StringBuilder sb new StringBuilder(); sb.append(CREATE SEQUENCE ).append(seqName) .append( START WITH 1 INCREMENT BY 1 NOCACHE;\n); sb.append(CREATE OR REPLACE TRIGGER ).append(triggerName) .append( BEFORE INSERT ON ).append(tableName) .append( FOR EACH ROW\n) .append(BEGIN\n) .append( IF :NEW.).append(columnName).append( IS NULL THEN\n) .append( SELECT ).append(seqName).append(.NEXTVAL INTO :NEW.) .append(columnName).append( FROM DUAL;\n) .append( END IF;\n) .append(END;\n/\n); return sb.toString(); } }逻辑说明序列名和触发器名按表名生成避免冲突。触发器里判断:NEW.column IS NULL是为了兼容显式插入主键的场景。参数上START WITH和INCREMENT BY要跟原表最大 ID 对齐迁移前最好先查一次SELECT MAX(id)否则新数据主键会撞车。如果目标库是 12c 以上可以直接生成GENERATED BY DEFAULT AS IDENTITY代码更短但要注意BY DEFAULT和ALWAYS的区别ALWAYS会拒绝显式插入。3.2 索引、注释和默认值三个最容易漏的细节索引转换相对直接但要注意 MySQL 的索引名在 Oracle 里是全局唯一的不同表可能有同名索引idx_name直接转会报ORA-00955。我的做法是给索引名加表名前缀比如IDX_USER_NAME。注释方面MySQL 的COMMENT写在字段后面Oracle 要用独立的COMMENT ON COLUMN语句必须拆成单独输出。默认值是最容易出玄学问题的地方。MySQL 的DEFAULT CURRENT_TIMESTAMP在 Oracle 里要写成DEFAULT SYSTIMESTAMP或DEFAULT CURRENT_TIMESTAMP但 Oracle 对DATETIME类型不接受CURRENT_TIMESTAMP作为默认值必须用TIMESTAMP类型。还有DEFAULT 0000-00-00 00:00:00这种 MySQL 特有的零值Oracle 直接报错需要在转换时替换成NULL或一个合法时间。// 处理默认值的片段 public static String convertDefault(String mysqlDefault, String oracleType) { if (mysqlDefault null) return ; if (mysqlDefault.equalsIgnoreCase(CURRENT_TIMESTAMP)) { // Oracle 中 TIMESTAMP 类型才支持 return oracleType.contains(TIMESTAMP) ? DEFAULT CURRENT_TIMESTAMP : ; } if (mysqlDefault.startsWith(0000-00-00)) { return DEFAULT NULL; // 零值在 Oracle 非法 } return DEFAULT mysqlDefault; }逻辑说明这个方法按默认值内容分支处理CURRENT_TIMESTAMP只在时间类型上保留零值统一降级为NULL。参数上要注意Oracle 的VARCHAR2默认值要带引号而NUMBER不能带所以调用前要先知道目标类型。3.3 批量转换的目录结构和执行入口一个能用的工具不能只有一个类。我一般会拆成parser、mapper、generator、validator四层入口用命令行接收输入目录和输出目录。# 编译并运行转换器 mvn clean package -DskipTests java -jar target/mysql2oracle.jar \ --input ./sql/mysql \ --output ./sql/oracle \ --config ./conf/type-mapping.yml \ --dialect oracle参数说明--input是 MySQL 脚本目录工具会遍历所有.sql文件--output是输出目录按原文件名加_oracle.sql后缀--config指定类型映射和命名规则--dialect目前固定 oracle留作以后扩展。执行后每个文件生成对应的 Oracle 脚本同时输出一份convert-report.log记录哪些字段被改了、哪些默认值被降级方便人工复核。4. 转换结果怎么验证别等导入报错才发现问题4.1 用 Oracle 的语法检查做第一道过滤生成的脚本不能直接往生产库灌。我一般先在测试库用EXPLAIN PLAN或直接执行 DDL看有没有ORA-报错。更轻量的做法是用 Oracle 的DBMS_SQL.PARSE做语法解析不实际建表。-- 在 Oracle 中检查 DDL 语法不实际执行 DECLARE c INTEGER; BEGIN c : DBMS_SQL.OPEN_CURSOR; DBMS_SQL.PARSE(c, CREATE TABLE test_t (id NUMBER(10), name VARCHAR2(64)), DBMS_SQL.NATIVE); DBMS_SQL.CLOSE_CURSOR(c); DBMS_OUTPUT.PUT_LINE(语法通过); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(语法错误: || SQLERRM); END; /逻辑说明DBMS_SQL.PARSE只解析不执行适合批量预检。参数上DBMS_SQL.NATIVE表示用当前数据库方言。把生成的每条 DDL 都过一遍能提前抓出类型不匹配、关键字冲突等问题。4.2 数据一致性怎么抽查行数和关键字段比对脚本转换完真正导入后还要验证数据。我一般会写一个对比脚本在 MySQL 和 Oracle 两边分别跑比对行数和几个关键字段的聚合值。-- MySQL 侧 SELECT COUNT(*) AS cnt, SUM(id) AS sum_id, MAX(created_at) AS max_time FROM user; -- Oracle 侧 SELECT COUNT(*) AS cnt, SUM(id) AS sum_id, MAX(created_at) AS max_time FROM user;两边结果要一致。如果SUM(id)对不上可能是自增序列起始值没对齐如果MAX(created_at)差几秒可能是时区或精度问题。这一步不能省我见过太多“脚本能建表但数据对不上”的案例。5. 避坑与排查转换工具落地时最容易踩的五个坑5.1 现象导入报 ORA-00907 缺失右括号原因MySQL 的ENUM(a,b)或SET类型在 Oracle 里没有对应解析器遇到括号里的逗号会错位。解决在类型映射阶段把ENUM转成VARCHAR2(20)加CHECK约束或者直接转VARCHAR2并在报告里标记需要人工确认。5.2 现象中文注释变成乱码原因MySQL 脚本是 UTF-8但 Oracle 客户端NLS_LANG设置不对或者VARCHAR2用了字节语义导致截断。解决连接串里显式指定字符集VARCHAR2统一用CHAR语义比如VARCHAR2(64 CHAR)并在导入前确认NLS_LANG与数据库字符集一致。5.3 现象索引名冲突报 ORA-00955原因不同表有同名索引Oracle 索引名全局唯一。解决生成索引时统一加表名前缀并在转换器里维护一个已用索引名集合遇到重复自动加后缀。5.4 现象DATETIME 默认值导致建表失败原因Oracle 的DATE类型不支持CURRENT_TIMESTAMP默认值必须用TIMESTAMP。解决类型映射时把DATETIME转成TIMESTAMP默认值同步改为CURRENT_TIMESTAMP如果原字段是DATE默认值降级为NULL并在报告里提示。5.5 现象脚本执行到一半报 ORA-00942 表不存在原因MySQL 脚本里表有创建顺序依赖或者外键引用顺序不对。解决转换器要保留原始顺序同时在生成外键时统一放到所有建表语句之后用ALTER TABLE ADD CONSTRAINT单独输出。6. 进阶技巧把转换规则做成可回归的测试集工具写完不是终点规则会随项目变。我的习惯是给转换器配一套回归测试准备一批 MySQL 输入脚本和对应的 Oracle 期望输出每次改映射规则就跑一遍看有没有意外改动。用 JUnit 加参数化测试就能做。import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; public class TypeMappingTest { ParameterizedTest CsvSource({ INT, NUMBER(10), BIGINT, NUMBER(19), VARCHAR(64), VARCHAR2(64 CHAR), DATETIME, TIMESTAMP, TEXT, CLOB }) void testTypeMapping(String mysqlType, String expectedOracleType) { assertEquals(expectedOracleType, TypeMapper.map(mysqlType)); } }逻辑说明CsvSource把输入输出写成表格改规则时先改测试再改代码。参数上要注意VARCHAR的CHAR语义必须显式写否则 Oracle 默认按字节算中文会截断。这套测试跑起来只要几秒但能挡住大部分低级错误。还有一个技巧是给转换器加一个--dry-run模式只输出报告不写文件方便在正式转换前快速看一遍哪些字段被改了。我一般会在报告里按“类型变更”“默认值降级”“注释拆分”三类分组人工扫一眼就能判断有没有问题。最后说个血泪教训别信“一次转换永久可用”。MySQL 脚本会随业务迭代转换规则也要跟着更新。把映射配置和测试集一起纳入版本管理每次上游脚本变了就重跑一遍比事后救火便宜得多。希望帮到你。本文还有配套的精品资源点击获取