Java实现MySQL转Oracle脚本转换工具:类型映射、自增与分页语法全解析
发布时间:2026/10/1 21:13:26来源:尧图网络
简介这是一款基于Java开发的数据库脚本转换工具面向需要进行MySQL到Oracle迁移的开发者与数据库管理员用于解决两种数据库在语法、数据类型、函数及存储过程上的兼容差异降低手工改写脚本的时间与出错风险。压缩包共34个文件约3.01MB包含11个java源文件、11个class编译文件、6个jar依赖包以及properties配置、md说明等其中jar涵盖MySQL与Oracle驱动、commons-dbutils、commons-io等便于直接运行或二次开发。工具核心流程包括读取MySQL的DDL与DML语句、完成语法与类型映射转换、特殊处理视图触发器存储过程并生成Oracle可执行脚本。目前已有183人学习下载适合研究SQL解析、数据库迁移与Java文件处理的实践者参考也可作为学习数据库兼容性问题的练手项目。1. 从 MySQL 到 Oracle 的脚本转换为什么手工改 200 张表迟早要翻车手上接过一个老系统迁移的活原库是 MySQL 5.7目标库是 Oracle 19c光业务表就两百多张加上索引、视图、存储过程导出脚本一万多行。第一次我图省事打算手工改——改到第三十张表就发现不对劲AUTO_INCREMENT要换成序列加触发器TINYINT(1)得映射成NUMBER(1)反引号全要换成双引号ENGINEInnoDB这种子句 Oracle 根本不认。改到一半人已经麻了还漏了两处LIMIT上线当天直接报错。这就是「基于 Java 的数据库脚本转换工具mysql-oracle」要解决的问题把 MySQL 导出的 DDL/DML 脚本自动翻译成 Oracle 能直接执行的脚本。它适合三类人——做异构数据库迁移的 DBA、需要把开源项目落到 Oracle 环境的后端、以及被「mysql 转 oracle」这种脏活反复折磨的 Java 工程师。核心不是写个正则替换就完事而是要把两边的类型系统、自增机制、分页语法、函数名差异都吃透再用一套可扩展的规则引擎跑起来。下面我按自己实际落地的路径把选型、实现、参数和踩过的坑讲清楚。2. 转换规则怎么定类型映射、自增与语法的三张对照表动手写代码之前先把规则理清楚。很多人一上来就写String.replace结果INT被替换成NUMBER之后BIGINT里的INT又被二次替换脚本直接废掉。规则必须先分类、再按优先级匹配这是整个工具的地基。2.1 数据类型映射别用字符串替换用词边界匹配MySQL 和 Oracle 的类型不是一一对应有些要拆有些要合并。下面这张表是我实际项目里验证过的映射覆盖了 90% 以上的常见字段MySQL 类型Oracle 类型说明TINYINT(1)NUMBER(1)常被当布尔用保留 1 位TINYINTNUMBER(3)有符号范围 -128~127SMALLINTNUMBER(5)INT / INTEGERNUMBER(10)BIGINTNUMBER(19)别用 NUMBER(20)19 位够用FLOATBINARY_FLOATDOUBLEBINARY_DOUBLEDECIMAL(p,s)NUMBER(p,s)精度直接搬VARCHAR(n)VARCHAR2(n)注意是 VARCHAR2TEXTCLOBDATETIME / TIMESTAMPTIMESTAMPDATEDATEOracle 的 DATE 含时分秒语义不同BLOBBLOB一致关键点是匹配顺序先匹配带括号的复合类型TINYINT(1)、DECIMAL(10,2)再匹配裸类型。用正则的\b词边界避免BIGINT被INT规则误伤。我一般会把规则做成有序列表从上往下第一个命中的生效。// 类型映射规则按顺序匹配先复合后简单 private static final ListTypeRule TYPE_RULES Arrays.asList( // 复合类型优先正则捕获括号内参数 new TypeRule(TINYINT\\(1\\), NUMBER(1)), new TypeRule(TINYINT, NUMBER(3)), new TypeRule(SMALLINT, NUMBER(5)), new TypeRule(BIGINT, NUMBER(19)), // 必须在 INT 之前 new TypeRule(INT(EGER)?, NUMBER(10)), new TypeRule(DECIMAL\\((\\d),(\\d)\\), NUMBER($1,$2)), new TypeRule(VARCHAR\\((\\d)\\), VARCHAR2($1)), new TypeRule(TEXT, CLOB), new TypeRule(DATETIME, TIMESTAMP) ); public String mapType(String mysqlType) { String upper mysqlType.toUpperCase().trim(); for (TypeRule rule : TYPE_RULES) { // 用词边界包裹防止 BIGINT 里的 INT 被单独命中 Pattern p Pattern.compile(\\b rule.pattern \\b, Pattern.CASE_INSENSITIVE); Matcher m p.matcher(upper); if (m.find()) { return m.replaceFirst(rule.oracle); } } return upper; // 未命中保持原样人工复核 }这段代码的逻辑是把类型规则做成有序列表BIGINT必须排在INT前面否则BIGINT会先被INT规则吃掉变成BIGNUMBER(10)。\\b词边界保证INT不会匹配到POINT、BIGINT这类词内部。参数说明上TypeRule的pattern是正则oracle是替换串$1、$2对应捕获组。未命中的类型原样返回交给人工复核比乱猜一个类型安全得多。2.2 自增主键AUTO_INCREMENT 要拆成序列加触发器这是 MySQL 转 Oracle 最典型的坑。MySQL 的AUTO_INCREMENT在 Oracle 里没有直接对应标准做法是「序列 触发器」或者 12c 以上的IDENTITY列。考虑到很多目标库还是 11g我一般用序列加触发器兼容性最好。原始 MySQL 建表CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;转换后要生成三段建表、建序列、建触发器。CREATE TABLE T_ORDER ( ID NUMBER(19) NOT NULL, ORDER_NO VARCHAR2(64) NOT NULL, PRIMARY KEY (ID) ); CREATE SEQUENCE SEQ_T_ORDER START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER TRG_T_ORDER BEFORE INSERT ON T_ORDER FOR EACH ROW WHEN (NEW.ID IS NULL) BEGIN SELECT SEQ_T_ORDER.NEXTVAL INTO :NEW.ID FROM DUAL; END; /转换逻辑上工具要能识别AUTO_INCREMENT关键字把它从列定义里摘掉同时记录「这张表有自增列、列名是什么」最后统一生成序列和触发器。序列名我用SEQ_加表名触发器用TRG_加表名方便回溯。NOCACHE是为了避免 RAC 环境下序列跳号单实例可以用CACHE 20提性能。触发器里的WHEN (NEW.ID IS NULL)保证显式插入 ID 时不覆盖这点在数据迁移阶段很重要。2.3 语法差异反引号、LIMIT、ENGINE 子句的批量清理除了类型和自增还有一批「语法噪音」要清掉。反引号全部换双引号ENGINEInnoDB、DEFAULT CHARSETutf8mb4、COLLATE...这些表选项整段删除UNSIGNED去掉Oracle 的 NUMBER 本身有符号靠约束保证非负。LIMIT是查询语句里的硬骨头Oracle 没有LIMIT得改写成ROWNUM或 12c 的FETCH FIRST。分页查询的转换最麻烦因为LIMIT offset, size和LIMIT size语义不同// LIMIT 转换区分 LIMIT n 和 LIMIT offset, n 两种形式 private String convertLimit(String sql) { // 形式一LIMIT offset, size - ROWNUM 双层嵌套 Matcher m1 Pattern.compile( LIMIT\\s(\\d)\\s*,\\s*(\\d), Pattern.CASE_INSENSITIVE).matcher(sql); if (m1.find()) { int offset Integer.parseInt(m1.group(1)); int size Integer.parseInt(m1.group(2)); String inner m1.replaceFirst(); // 外层限制上界内层过滤下界 return SELECT * FROM (SELECT A.*, ROWNUM RN FROM ( inner ) A WHERE ROWNUM (offset size) ) WHERE RN offset; } // 形式二LIMIT size - 直接套 ROWNUM Matcher m2 Pattern.compile( LIMIT\\s(\\d), Pattern.CASE_INSENSITIVE).matcher(sql); if (m2.find()) { String size m2.group(1); return SELECT * FROM ( m2.replaceFirst() ) WHERE ROWNUM size; } return sql; }逻辑说明LIMIT offset, size要转成双层嵌套内层先取offset size条外层再用RN offset砍掉前 offset 条这是 Oracle 分页的经典写法。参数上offset是起始行从 0 开始size是每页条数。注意ROWNUM的比较必须放在内层写成WHERE ROWNUM offset是永远查不出数据的这是新手最容易踩的坑。如果目标库是 12c 以上可以直接用OFFSET n ROWS FETCH NEXT m ROWS ONLY语法更干净但兼容性差一些。3. 用 Java 把转换引擎搭起来从读文件到写脚本的完整链路规则理清楚之后就是工程实现。我一般用 Maven 建一个独立的小工具不依赖 Spring 那套重家伙主类加几个工具类就能跑。核心链路是读 MySQL 脚本 → 按语句切分 → 逐条识别类型DDL/DML→ 套用对应规则 → 拼接输出 → 写 Oracle 脚本。3.1 语句切分分号不是唯一分隔符MySQL 脚本里分号是语句结束符但字符串字面量里的分号、注释里的分号不能当分隔符。直接split(;)会把INSERT INTO t VALUES (a;b)切坏。稳妥做法是逐字符扫描跟踪引号状态和注释状态。// 按分号切分 SQL跳过字符串和注释里的分号 public static ListString splitStatements(String script) { ListString result new ArrayList(); StringBuilder cur new StringBuilder(); boolean inSingle false, inDouble false, inLineComment false; for (int i 0; i script.length(); i) { char c script.charAt(i); char next (i 1 script.length()) ? script.charAt(i 1) : \0; // 行注释-- 开头到行尾 if (!inSingle !inDouble c - next -) { inLineComment true; } if (inLineComment c \n) { inLineComment false; } if (!inLineComment) { if (c \ !inDouble) inSingle !inSingle; if (c !inSingle) inDouble !inDouble; // 只有不在引号内、不在注释内的分号才是语句边界 if (c ; !inSingle !inDouble) { result.add(cur.toString().trim()); cur.setLength(0); continue; } } cur.append(c); } if (cur.toString().trim().length() 0) { result.add(cur.toString().trim()); } return result; }这段扫描器的关键是三个状态位inSingle、inDouble、inLineComment。只有三个状态都为 false 时遇到的分号才算语句边界。参数上没什么可调的但要注意 MySQL 的/* */块注释这里没处理如果你的脚本里有块注释得再加一个inBlockComment状态位。切分完之后每条语句单独送进转换器避免跨语句的正则误匹配。3.2 转换主流程识别语句类型再分派切分好的语句要分类处理。CREATE TABLE走建表转换INSERT走数据转换CREATE INDEX走索引转换其他语句原样保留或简单清理。用策略模式每种语句一个处理器主流程只负责分派。public class ScriptConverter { private final MapPattern, StatementHandler handlers new LinkedHashMap(); public ScriptConverter() { // 顺序敏感CREATE TABLE 必须在 CREATE INDEX 之前判断 handlers.put(Pattern.compile(^CREATE\\sTABLE, Pattern.CASE_INSENSITIVE), new CreateTableHandler()); handlers.put(Pattern.compile(^CREATE\\s(UNIQUE\\s)?INDEX, Pattern.CASE_INSENSITIVE), new CreateIndexHandler()); handlers.put(Pattern.compile(^INSERT\\sINTO, Pattern.CASE_INSENSITIVE), new InsertHandler()); handlers.put(Pattern.compile(^DROP\\sTABLE, Pattern.CASE_INSENSITIVE), new DropTableHandler()); } public String convert(String mysqlScript) { ListString statements SqlSplitter.splitStatements(mysqlScript); StringBuilder out new StringBuilder(); for (String stmt : statements) { String converted dispatch(stmt); if (converted ! null !converted.isEmpty()) { out.append(converted).append(;\n\n); } } return out.toString(); } private String dispatch(String stmt) { for (Map.EntryPattern, StatementHandler e : handlers.entrySet()) { if (e.getKey().matcher(stmt).find()) { return e.getValue().handle(stmt); } } return stmt; // 未识别语句原样输出人工复核 } }逻辑上LinkedHashMap保证处理器按插入顺序匹配CREATE TABLE排在CREATE INDEX前面避免CREATE TABLE被索引规则误判。每个StatementHandler内部再调用前面讲的类型映射、自增处理、语法清理。参数说明convert接收整段脚本字符串返回转换后的 Oracle 脚本。未识别的语句原样输出这是有意的——宁可让工程师看到原文也不要工具自作主张改错。3.3 输出与编码别让中文注释变成乱码写文件这一步看着简单翻车的人不少。MySQL 脚本常见utf8mb4编码Oracle 客户端默认可能是GBK或AL32UTF8编码不对中文注释全变问号。我一般统一用 UTF-8 读写输出文件加 BOM 头可选方便 Windows 下的 PL/SQL Developer 识别。// 统一 UTF-8 读写避免中文注释乱码 public static void writeScript(String content, String outputPath) throws IOException { // 显式指定 UTF-8不依赖平台默认编码 try (BufferedWriter writer new BufferedWriter( new OutputStreamWriter( new FileOutputStream(outputPath), StandardCharsets.UTF_8))) { writer.write(content); } }参数上StandardCharsets.UTF_8必须显式写不能用new FileWriter(path)那个用的是平台默认编码在 Windows 上就是 GBK跨平台必翻车。如果目标环境是 Oracle 的AL32UTF8字符集UTF-8 文件直接script.sql执行没问题如果是ZHS16GBK得在客户端设置NLS_LANG匹配否则还是乱码。4. 避坑与排查转换工具上线前必须过的五道坎工具写完了不代表能用真正折磨人的是各种边界情况。下面这五条是我在实际迁移里踩出来的每条都按「现象 → 原因 → 解决」记下来你照着排查能省不少时间。4.1 现象脚本执行报 ORA-00907 缺失右括号原因MySQL 的KEY idx_name (col)这种内联索引定义Oracle 建表语句里不认必须拆成独立的CREATE INDEX。工具如果只做类型替换这段会原样输出Oracle 解析到KEY就报错。解决在CreateTableHandler里识别KEY、UNIQUE KEY、INDEX开头的行从建表语句里摘出来收集到索引列表建表语句结束后统一生成CREATE INDEX语句。注意主键PRIMARY KEY要保留在建表语句里别一起摘了。4.2 现象插入数据时主键冲突或序列不连续原因迁移时先INSERT了带显式 ID 的历史数据序列还停在 1后续插入直接撞主键。或者触发器没加WHEN (NEW.ID IS NULL)显式插入的 ID 被序列值覆盖。解决数据迁移完成后必须把序列的当前值重置到表里最大 ID 之上。执行ALTER SEQUENCE SEQ_T_ORDER RESTART START WITH 10001;其中 10001 是SELECT MAX(ID)1 FROM T_ORDER的结果。这一步工具不会自动做得写进迁移 checklist。4.3 现象日期字段查出来时分秒全是 00:00:00原因MySQL 的DATE类型只存日期Oracle 的DATE类型含时分秒。如果 MySQL 里用DATE存了纯日期转到 Oracle 后语义变了某些按时间范围查询的 SQL 会漏数据。解决如果业务上确实只需要日期Oracle 侧改用DATE但插入时用TRUNC(SYSDATE)如果业务需要时分秒MySQL 侧本来就该用DATETIME转换时映射成TIMESTAMP。这个要在转换前跟业务确认清楚工具层面只能按类型映射语义得人来定。4.4 现象GROUP_CONCAT转换后报 ORA-00904 无效标识符原因GROUP_CONCAT是 MySQL 特有函数Oracle 里对应的是LISTAGG语法还不一样MySQL 是GROUP_CONCAT(col SEPARATOR ,)Oracle 是LISTAGG(col, ,) WITHIN GROUP (ORDER BY col)。解决在函数转换规则里加一条GROUP_CONCAT到LISTAGG的映射注意SEPARATOR关键字要转成逗号参数还要补上WITHIN GROUP子句。类似的还有IFNULL转NVL、NOW()转SYSDATE、SUBSTRING转SUBSTR这些函数映射建议单独维护一张表。4.5 现象脚本文件太大PL/SQL Developer 执行到一半卡死原因一次性执行上万行脚本客户端内存扛不住或者某条语句报错后整个脚本中断前面的执行结果也没提交。解决把大脚本按表拆成多个小文件每个文件对应一张表的建表加索引执行完手动或自动COMMIT。工具输出时支持按表分文件文件名用表名方便定位问题。另外在脚本开头加SET DEFINE OFF避免符号被当成变量提示符。5. 让转换工具真正好用规则外置与回归验证的两个技巧工具能跑通只是及格线要让它在你团队里长期用下去还得解决两个问题规则怎么改不用重新编译以及怎么保证改完规则没把之前对的搞坏。第一个技巧是规则外置。把类型映射、函数映射、关键字清理都写成 JSON 或 YAML 配置文件工具启动时加载。这样 DBA 发现某个类型映射不对改配置文件就行不用找开发重新打包。配置文件结构大概是{typeRules: [{pattern: ..., replace: ...}], functionRules: [...]}加载时按数组顺序匹配和代码里的有序列表一个道理。我一般还会加一个dryRun开关只输出转换报告不写文件方便先看效果。第二个技巧是回归验证。准备一批「输入 MySQL 脚本 期望 Oracle 脚本」的测试用例每次改规则跑一遍单元测试对比输出。测试用例不用多覆盖典型场景就行带自增的表、带内联索引的表、带LIMIT的查询、带GROUP_CONCAT的聚合。用 JUnit 的assertEquals逐字符比对差异一目了然。这一步能挡住 80% 的规则回归问题比上线后才发现强太多。最后一个习惯转换完的脚本别直接在生产库跑。先在测试库执行一遍用SELECT COUNT(*)核对每张表的行数用USER_TAB_COLUMNS核对字段类型用USER_INDEXES核对索引数量。数据对不上就回滚重来别抱侥幸心理。我吃过一次亏转换脚本里漏了一张关联表的外键测试库没数据没暴露生产上线后关联查询全空排查了整整一下午。从那以后我的规矩是转换工具的输出必须过一遍自动化核对脚本人工抽查只作为补充。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网