简介这是一款基于Java开发的数据库脚本转换工具面向需要将MySQL脚本迁移至Oracle环境的开发者与运维人员可解决两种数据库在语法、数据类型、函数及存储过程上的兼容差异降低手工改写脚本的时间与出错风险。资源包共34个文件约3.01MB以11个java源码与11个class编译文件为核心辅以6个jar依赖库、2个properties配置及md说明文档并包含工程配置文件便于直接导入IDE运行或二次开发。工具核心流程涵盖读取MySQL的DDL与DML语句、完成语法与类型映射转换、特殊处理视图触发器存储过程最终生成Oracle可执行脚本。已有183人学习下载适合学习数据库迁移、SQL解析转换及Java文件处理的中高级开发者参考也可作为理解跨数据库兼容性问题的实践项目。1. 从 MySQL 到 Oracle 的脚本迁移为什么手工改 SQL 迟早要翻车做过一次 MySQL 到 Oracle 的库迁移你就会明白这事有多磨人。表结构、字段类型、自增主键、分页语法、函数名、日期格式、字符串拼接几乎每一处都埋着差异。手工改一个几十张表的库改到一半就开始怀疑人生改完之后还不敢保证没有漏网之鱼。基于 Java 的数据库脚本转换工具解决的就是这个场景把 MySQL 的建表语句、DML、部分存储过程脚本按规则批量转成 Oracle 能跑的版本。它适合谁适合手上有一坨 MySQL 脚本要迁到 Oracle、又不想纯靠正则硬怼的 Java 开发或 DBA。核心思路不是「翻译 SQL」而是「解析 SQL 结构再按目标方言重新生成」这一点决定了工具能做到什么程度也决定了它的边界在哪。2. 转换工具的技术选型为什么用 Java 而不是脚本语言2.1 用 ANTLR 还是正则解析方式决定天花板先说结论正则能覆盖 60% 的简单场景但剩下 40% 会让你在深夜对着报错发呆。MySQL 和 Oracle 的语法差异不是简单的字符串替换比如LIMIT 0,10要变成ROWNUM嵌套AUTO_INCREMENT要变成序列加触发器IFNULL要变成NVL这些如果只靠正则遇到嵌套子查询、多表 JOIN、函数嵌套就会崩。常见做法是用 ANTLR4 加载 MySQL 的语法文件MySqlParser.g4生成 AST再写一个 Visitor 遍历树把 MySQL 节点映射成 Oracle 节点。这样做的好处是你能准确知道当前解析到的是LIMIT子句还是普通标识符不会误伤字段名里带limit的列。选型理由很直接Java 生态里有成熟的 ANTLR 运行时也有现成的 MySQL 语法文件社区维护得不错。用 Python 也能做但如果你团队是 Java 栈后续要集成到 Spring Boot 服务里做在线转换Java 更顺。2.2 最小可跑通的转换器骨架下面这段代码是一个最小可运行的转换器入口用 ANTLR 解析 MySQL 脚本然后走 Visitor 输出 Oracle 语句。依赖只需要在pom.xml里加antlr4-runtime和对应版本的 MySQL 语法包。// MySqlToOracleConverter.java import org.antlr.v4.runtime.*; import org.antlr.v4.runtime.tree.*; public class MySqlToOracleConverter { public static String convert(String mysqlScript) { // 1. 词法分析 MySqlLexer lexer new MySqlLexer(CharStreams.fromString(mysqlScript)); CommonTokenStream tokens new CommonTokenStream(lexer); // 2. 语法分析生成 AST MySqlParser parser new MySqlParser(tokens); ParseTree tree parser.root(); // 3. 遍历 AST按 Oracle 方言重写 ParseTreeWalker walker new ParseTreeWalker(); OracleRewriteListener listener new OracleRewriteListener(); walker.walk(listener, tree); return listener.getOutput(); } public static void main(String[] args) { String mysql CREATE TABLE user (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));; System.out.println(convert(mysql)); } }逻辑说明MySqlLexer把字符流转成 token 流MySqlParser按语法规则建树OracleRewriteListener是自定义监听器在进入和退出特定节点时改写输出。参数方面CharStreams.fromString适合小脚本如果是大文件换成CharStreams.fromFileName并配合BufferedTokenStream减少内存占用。2.3 类型映射表别靠记忆靠配置字段类型映射是最容易出错的地方。建议把映射关系抽成配置文件而不是硬编码在 Java 里。下面这张表是我实际迁移时用的基础映射覆盖了 90% 的常见类型。MySQL 类型Oracle 类型备注INT / INTEGERNUMBER(10)无符号要单独处理BIGINTNUMBER(19)超过 19 位考虑 NUMBER(38)VARCHAR(n)VARCHAR2(n CHAR)n 超过 4000 用 CLOBTEXTCLOB注意 Oracle 不支持 TEXTDATETIMETIMESTAMP精度默认 6DATEDATEOracle DATE 含时分秒TINYINT(1)NUMBER(1)布尔语义需应用层处理DECIMAL(p,s)NUMBER(p,s)直接对应AUTO_INCREMENT序列 触发器见下一节配置化之后遇到新类型只需要改 JSON 或 YAML不用重新编译。我一般会把这张表放在resources/type-mapping.json启动时加载到MapString, String里。3. 核心转换规则落地从建表到 DML 的逐条拆解3.1 自增主键怎么转成序列加触发器MySQL 的AUTO_INCREMENT在 Oracle 里没有直接对应标准做法是建序列再建触发器在插入前赋值。转换工具要做的不是简单替换关键字而是识别出带AUTO_INCREMENT的列生成额外的 DDL。// 在 Visitor 的 exitColumnDefinition 里处理 if (ctx.AUTO_INCREMENT() ! null) { String tableName currentTable; String columnName ctx.columnName().getText(); String seqName SEQ_ tableName.toUpperCase(); String triggerName TRG_ tableName.toUpperCase(); // 生成序列 output.append(CREATE SEQUENCE ).append(seqName) .append( START WITH 1 INCREMENT BY 1 NOCACHE;\n); // 生成触发器 output.append(CREATE OR REPLACE TRIGGER ).append(triggerName) .append( BEFORE INSERT ON ).append(tableName) .append( FOR EACH ROW WHEN (NEW.).append(columnName).append( IS NULL)) .append( BEGIN SELECT ).append(seqName) .append(.NEXTVAL INTO :NEW.).append(columnName) .append( FROM DUAL; END;\n); }逻辑说明exitColumnDefinition在解析完一个列定义后触发此时能拿到列名和是否带自增标记。序列名和触发器名按表名生成避免冲突。参数上START WITH 1和INCREMENT BY 1是默认值如果原表有偏移量需求可以从 MySQL 的AUTO_INCREMENT当前值读出来填进去。注意触发器里的WHEN (NEW.column IS NULL)是为了兼容手动指定 ID 的插入不加的话手动插入会被序列覆盖。3.2 分页语句的三种改写方式MySQL 的LIMIT offset, size在 Oracle 里要改写成嵌套子查询。常见有三种写法工具里我一般选第二种兼容性最好。-- 原始 MySQL SELECT * FROM orders ORDER BY create_time DESC LIMIT 20, 10; -- 改写方式一ROWNUM 两层嵌套Oracle 11g 及以前 SELECT * FROM ( SELECT a.*, ROWNUM rn FROM ( SELECT * FROM orders ORDER BY create_time DESC ) a WHERE ROWNUM 30 ) WHERE rn 20; -- 改写方式二OFFSET FETCHOracle 12c 及以上 SELECT * FROM orders ORDER BY create_time DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;工具里怎么选如果目标库确定是 12c 以上直接用OFFSET FETCH代码简单不易错。如果还要兼容 11g就得走ROWNUM嵌套。我一般会在配置里加一个oracle.version参数默认12c遇到老库再切回11g模式。注意ROWNUM的过滤顺序先内层排序再中层限制上界最后外层取下界顺序写反了结果就是错的。3.3 函数与运算符的映射清单函数替换是高频操作但别小看它IFNULL和NVL看起来一样遇到IFNULL(a, b)里 a 是子查询时直接文本替换可能把括号搞乱。建议在 AST 层面识别函数调用节点再替换。MySQL 函数/运算符Oracle 对应注意点IFNULL(a,b)NVL(a,b)参数个数一致CONCAT(a,b)a || bOracle 的 CONCAT 只支持两个参数NOW()SYSDATE精度不同SYSDATE 到秒DATE_FORMAT(d,f)TO_CHAR(d,f)格式串要转换%Y 变 YYYYSTR_TO_DATE(s,f)TO_DATE(s,f)同上LIMITROWNUM / FETCH见分页节 反引号无Oracle 用双引号或不用ENGINEInnoDB无直接删掉DATE_FORMAT的格式串转换是个坑MySQL 用%Y-%m-%dOracle 用YYYY-MM-DD需要建一张格式符映射表逐字符替换。我一般写一个FormatConverter类专门干这个不混在 SQL 重写逻辑里。4. 避坑与排查迁移现场最常见的五类翻车4.1 现象转换后的建表语句在 Oracle 报 ORA-00907 缺少右括号原因MySQL 的ENUM或SET类型没有对应转换工具原样输出导致语法错误。解决在类型映射表里把ENUM映射成VARCHAR2(255)并在应用层加约束检查或者单独生成CHECK约束。4.2 现象数据插入时主键冲突序列值从 1 开始但表里已有数据原因序列创建时没有设置START WITH为当前最大 ID 加一。解决转换工具在生成序列 DDL 前先查一次源表MAX(id)把值写进START WITH。如果源库连不上就在输出脚本里加注释提醒手工调整。4.3 现象日期字段查出来多了 8 小时或少了几小时原因MySQLDATETIME不带时区OracleTIMESTAMP可能受数据库时区影响。解决统一用TIMESTAMP不带时区或者在转换时把DATETIME映射成DATEOracle DATE 含时分秒并在连接串里固定时区。4.4 现象GROUP BY查询在 Oracle 报 ORA-00979 不是 GROUP BY 表达式原因MySQL 允许SELECT列表出现非聚合、非分组列Oracle 严格模式不允许。解决转换工具无法自动推断业务语义只能在输出时检测这种模式并打警告让开发手工改。我一般会在日志里输出「第 X 行疑似宽松 GROUP BY请人工确认」。4.5 现象脚本里的注释#和--混用导致解析失败原因MySQL 支持#注释Oracle 只认--和/* */。解决在词法分析阶段就把#开头的行统一转成--或者直接在预处理里删掉。注意字符串里的#不能动所以要在 token 层面处理不能全局替换。5. 进阶技巧用测试用例反向验证转换质量工具写完只是开始怎么证明它转得对才是关键。我的习惯是建一个「双跑对比」流程同一批业务 SQL在 MySQL 和转换后的 Oracle 上分别执行比对结果集。下面是一个简化的对比脚本骨架。// DualRunValidator.java public class DualRunValidator { public void validate(String mysqlSql, String oracleSql) { ListMapString, Object mysqlResult runOnMysql(mysqlSql); ListMapString, Object oracleResult runOnOracle(oracleSql); if (mysqlResult.size() ! oracleResult.size()) { System.out.println(行数不一致: MySQL mysqlResult.size() , Oracle oracleResult.size()); return; } for (int i 0; i mysqlResult.size(); i) { if (!mysqlResult.get(i).equals(oracleResult.get(i))) { System.out.println(第 i 行不一致); System.out.println(MySQL: mysqlResult.get(i)); System.out.println(Oracle: oracleResult.get(i)); } } } }逻辑说明runOnMysql和runOnOracle分别用 JDBC 连两个库执行结果集转成ListMap再逐行比对。参数上注意排序稳定性——如果 SQL 没有ORDER BY两次查询顺序可能不同比对前要先按主键排序。另外浮点数精度差异会导致equals失败金额字段建议用BigDecimal的compareTo而不是equals。这个验证流程跑一遍基本能暴露 80% 的转换错误。剩下的 20% 是存储过程、触发器等复杂对象只能靠人工 review。我一般会把验证不通过的 SQL 收集起来反哺到转换规则里迭代几轮之后准确率会明显上升。说个血泪经验别指望一次转换就 100% 可用工具的价值是把 90% 的机械劳动干掉让你集中精力处理那 10% 的硬骨头。我现在的习惯是转换完先跑一遍 DDL再跑一遍 DML最后拿双跑对比兜底三关过完才敢往生产环境推。希望帮到你。本文还有配套的精品资源点击获取