☰
Java实现MySQL转Oracle脚本转换工具:类型映射、自增与分页语法全解析
2026/10/1 21:13:25 网站建设 项目流程

简介:这是一款基于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),反引号全要换成双引号,ENGINE=InnoDB这种子句 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~127
SMALLINTNUMBER(5)
INT / INTEGERNUMBER(10)
BIGINTNUMBER(19)别用 NUMBER(20),19 位够用
FLOATBINARY_FLOAT
DOUBLEBINARY_DOUBLE
DECIMAL(p,s)NUMBER(p,s)精度直接搬
VARCHAR(n)VARCHAR2(n)注意是 VARCHAR2
TEXTCLOB
DATETIME / TIMESTAMPTIMESTAMP
DATEDATEOracle 的 DATE 含时分秒,语义不同
BLOBBLOB一致

关键点是匹配顺序:先匹配带括号的复合类型(TINYINT(1)、DECIMAL(10,2)),再匹配裸类型。用正则的\b词边界,避免BIGINT被INT规则误伤。我一般会把规则做成有序列表,从上往下第一个命中的生效。

// 类型映射规则:按顺序匹配,先复合后简单 private static final List<TypeRule> 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

转换后要生成三段:建表、建序列、建触发器。

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 子句的批量清理

除了类型和自增,还有一批「语法噪音」要清掉。反引号`全部换双引号,ENGINE=InnoDB、DEFAULT CHARSET=utf8mb4、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 List<String> splitStatements(String script) { List<String> 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 Map<Pattern, StatementHandler> handlers = new LinkedHashMap<>(); public ScriptConverter() { // 顺序敏感:CREATE TABLE 必须在 CREATE INDEX 之前判断 handlers.put(Pattern.compile("^CREATE\\s+TABLE", Pattern.CASE_INSENSITIVE), new CreateTableHandler()); handlers.put(Pattern.compile("^CREATE\\s+(UNIQUE\\s+)?INDEX", Pattern.CASE_INSENSITIVE), new CreateIndexHandler()); handlers.put(Pattern.compile("^INSERT\\s+INTO", Pattern.CASE_INSENSITIVE), new InsertHandler()); handlers.put(Pattern.compile("^DROP\\s+TABLE", Pattern.CASE_INSENSITIVE), new DropTableHandler()); } public String convert(String mysqlScript) { List<String> 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.Entry<Pattern, 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核对索引数量。数据对不上就回滚重来,别抱侥幸心理。我吃过一次亏,转换脚本里漏了一张关联表的外键,测试库没数据没暴露,生产上线后关联查询全空,排查了整整一下午。从那以后,我的规矩是:转换工具的输出必须过一遍自动化核对脚本,人工抽查只作为补充。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询