信创改造实战:用 AnyLine 将 Oracle 存储过程迁移到达梦的踩坑实录
一套基于 AnyLine 动态数据源 + 元数据驱动的 Oracle → 达梦存储过程迁移方法论,附完整代码与真实踩坑记录
一、信创改造背景
2024 年以来,信创(信息技术应用创新)已从"试点探索"进入"全面铺开"阶段。在党政、金融、电信、能源等关键行业,Oracle 到国产数据库的迁移已从"要不要做"变成了"怎么做更快更稳"。其中,达梦 DM8 作为国产数据库的头部产品,凭借其对 Oracle 语法的高度兼容,成为众多项目的首选目标库。
但"兼容"不等于"一致"。达梦通过 COMPATIBLE_MODE=2 提供了 Oracle 兼容模式,能覆盖大部分基础 SQL 场景。一旦涉及到存储过程、系统包、游标变量、异常处理等深水区,差异就会成倍放大。一个中大型系统的存储过程动辄上百个,每个过程中嵌套着 Oracle 特有的 PL/SQL 语法、系统包调用、隐式类型转换,逐一手动改写的工作量和风险都极高。
我们的项目是一个政务系统的信创改造,核心挑战:87 个存储过程(含 23 个 PACKAGE),涉及复杂的游标操作、动态 SQL、系统包调用,需要在保证业务逻辑不变的前提下,全部迁移到达梦 DM8。
本文将结合实际迁移经验,详细记录每一个踩坑点,并展示如何利用 AnyLine 的动态数据源 + 元数据驱动能力,大幅提升迁移效率和准确性。(在数据类型、系统函数的识别与转换过程中AnyLine内部实际已提供了相关方法。以下代码手写了一部分仅作参考。)
二、迁移核心挑战
Oracle 到达梦的存储过程迁移,不是简单的"翻译"工作。挑战分布在三个层面:
2.1 数据类型映射——看起来一样,用起来不同
| Oracle 类型 | 达梦推荐类型 | 踩坑点 | 风险等级 |
|---|---|---|---|
VARCHAR2(n) |
VARCHAR(n) |
Oracle 按字节计算长度,达梦按字符计算;中文场景实际容量差异 3 倍 | 中 |
NUMBER(p,s) |
DECIMAL(p,s) |
NUMBER 不带精度时可存浮点,DECIMAL 默认精度不同 | 中 |
DATE |
TIMESTAMP |
Oracle DATE 含时分秒,达梦 DATE 不含时分秒!必须改用 TIMESTAMP | 高 |
CLOB |
TEXT / CLOB |
CLOB 操作函数不完全兼容,DBMS_LOB 包差异大 | 中 |
RAW / LONG RAW |
VARBINARY |
UTL_RAW 包部分方法缺失 | 高 |
BFILE |
无直接对应 | 需要完全重写文件操作逻辑 | 高 |
很多迁移项目在这上面栽了跟头。Oracle 的 DATE 类型包含世纪、年、月、日、时、分、秒,而达梦的 DATE 只包含年月日。迁移前必须全局排查所有 DATE 字段,统一改为 TIMESTAMP,否则时间精度丢失导致的业务逻辑错误极难排查。
2.2 语法差异——魔鬼在细节
-- 1. 分页查询
SELECT * FROM (
SELECT t.*, ROWNUM rn
FROM orders t
WHERE ROWNUM <= 20
) WHERE rn > 10;
-- 2. 条件判断
SELECT DECODE(status,
1, '激活',
0, '禁用',
'未知'
) FROM users;
-- 3. 空值处理
SELECT NVL(phone, '无') FROM contacts;
-- 4. 当前时间
SELECT SYSDATE FROM DUAL;
-- 5. 外连接
SELECT a.name, b.dept_name
FROM emp a, dept b
WHERE a.dept_id = b.id(+);
-- 6. 变量赋值(两种方式都行)
SET v_sql = 'SELECT 1';
v_sql := 'SELECT 1';
-- 1. 分页查询
SELECT * FROM orders
ORDER BY id
LIMIT 10 OFFSET 10;
-- 2. 条件判断
SELECT CASE status
WHEN 1 THEN '激活'
WHEN 0 THEN '禁用'
ELSE '未知'
END FROM users;
-- 3. 空值处理
SELECT IFNULL(phone, '无') FROM contacts;
-- 或 COALESCE()
-- 4. 当前时间
SELECT CURRENT_TIMESTAMP;
-- 达梦也支持 FROM DUAL
-- 5. 外连接
SELECT a.name, b.dept_name
FROM emp a
LEFT JOIN dept b ON a.dept_id = b.id;
-- 6. 变量赋值(仅支持 :=)
v_sql := 'SELECT 1';
-- SET v_sql = ... ✗ 不支持
| 语法点 | Oracle | 达梦 DM8 | 改写难度 |
|---|---|---|---|
| PACKAGE(包) | 原生支持 PACKAGE / PACKAGE BODY | 拆成独立存储过程+函数,去掉包体 | ★★★ |
| CONNECT BY 递归 | CONNECT BY PRIOR ... START WITH | 替换为 WITH RECURSIVE CTE | ★★★ |
| 异常处理 | EXCEPTION WHEN ... THEN | 语法类似但预定义异常名不同 | ★★☆ |
| SYS_REFCURSOR | 原生支持游标变量 | 支持但语法略有差异 | ★★☆ |
| 变量赋值 | SET 和 := 都支持 | 仅支持 := | ★☆☆ |
| 表别名 AS | SELECT * FROM emp AS e | 表别名不能用 AS,列别名可以 | ★☆☆ |
| 存储过程调用 | call 可省略 | 必须使用 CALL 关键字 | ★☆☆ |
| CURSOR 子查询 | SELECT CURSOR(...) FROM ... | 不支持,需在应用层拆分 | ★★★ |
2.3 存储过程改造——工作量最大、风险最高
存储过程改造是整个迁移工作中工作量最大、风险最高的环节。难点集中在以下几个方面:
- PACKAGE 拆解:一个包体里几十个过程和函数互相调用,还有包级变量,需要理清依赖关系后逐个拆分
- 动态 SQL 适配:
EXECUTE IMMEDIATE在达梦中行为有差异,特别是绑定变量和返回值的处理 - 系统包替代:
DBMS_CRYPTO、UTL_I18N、UTL_ENCODE等包在达梦中部分缺失或功能不全 - DDL + DML 混用:存储过程中先执行 DDL(如创建索引)再执行 DML(如 INSERT),达梦会报"对象定义被修改"
- 隐式类型转换:Oracle 的大量隐式转换在达梦中可能失效或产生不同结果
三、真实踩坑实录
以下是我们在实际项目中遇到的典型问题,每一个都花了至少半天到一周的时间来解决。希望这些经验能帮你少走弯路。
坑1: "对象定义被修改"——DDL 和 DML 不能混用
现象:存储过程编译可以通过,但执行时报错"对象定义被修改"。排查发现,过程中先对一张表执行了 DROP INDEX(DDL),然后又执行了 INSERT(DML)。
根因:达梦数据库会预先获取 SQL 的所有执行计划再执行。当过程中先修改了表定义(DDL),字典信息发生变化,后续 DML 的执行计划与实际不一致,就触发了这个错误。
CREATE OR REPLACE PROCEDURE rebuild_idx
(p_table IN VARCHAR2) AS
BEGIN
-- 先 DDL:删除旧索引
EXECUTE IMMEDIATE
'DROP INDEX idx_' || p_table;
-- 再 DML:插入日志 ← Oracle 没问题
INSERT INTO op_log
(table_name, op_type, op_time)
VALUES
(p_table, 'REBUILD', SYSDATE);
-- 再 DDL:重建索引
EXECUTE IMMEDIATE
'CREATE INDEX idx_' || p_table ||
' ON ' || p_table || '(create_time)';
END;
CREATE OR REPLACE PROCEDURE rebuild_idx
(p_table IN VARCHAR2) AS
BEGIN
-- DDL 用动态 SQL
EXECUTE IMMEDIATE
'DROP INDEX idx_' || p_table;
-- ★ 关键改动:DML 也用动态执行
EXECUTE IMMEDIATE
'INSERT INTO op_log
(table_name, op_type, op_time)
VALUES
(''' || p_table || ''',
''REBUILD'',
SYSDATE)';
EXECUTE IMMEDIATE
'CREATE INDEX idx_' || p_table ||
' ON ' || p_table || '(create_time)';
END;
💡 经验总结:在达梦存储过程中,如果同一个过程里既有 DDL 又有 DML,必须把所有 DML 改成 EXECUTE IMMEDIATE 动态执行,因为动态执行时才生成执行计划,不会受 DDL 影响。
坑2: PACKAGE 拆解——包级变量是最大的坑
现象:Oracle 中有一个 PKG_ORDER 包,包含 12 个存储过程和 8 个函数,其中有一个包级变量 g_batch_id 被多个过程共享。直接拆分成独立存储过程后,业务逻辑断裂。
CREATE OR REPLACE PACKAGE pkg_order AS
-- 包级变量(所有过程共享)
g_batch_id NUMBER := 0;
g_error_cnt NUMBER := 0;
PROCEDURE init_batch;
PROCEDURE add_item(p_product_id NUMBER,
p_qty NUMBER);
PROCEDURE commit_batch;
FUNCTION get_batch_status
RETURN VARCHAR2;
END pkg_order;
CREATE OR REPLACE PACKAGE BODY pkg_order AS
PROCEDURE init_batch IS
BEGIN
g_batch_id := seq_batch.NEXTVAL;
g_error_cnt := 0;
INSERT INTO batch_header
(batch_id, status, create_time)
VALUES
(g_batch_id, 'OPEN', SYSDATE);
END;
PROCEDURE add_item(
p_product_id NUMBER,
p_qty NUMBER
) IS
BEGIN
INSERT INTO batch_detail
(batch_id, product_id, qty)
VALUES
(g_batch_id, p_product_id, p_qty);
-- ↑ 直接引用包级变量
EXCEPTION
WHEN OTHERS THEN
g_error_cnt := g_error_cnt + 1;
END;
PROCEDURE commit_batch IS
BEGIN
UPDATE batch_header
SET status = 'DONE',
item_count = g_error_cnt
WHERE batch_id = g_batch_id;
COMMIT;
END;
FUNCTION get_batch_status
RETURN VARCHAR2 IS
v_status VARCHAR2(20);
BEGIN
SELECT status INTO v_status
FROM batch_header
WHERE batch_id = g_batch_id;
RETURN v_status;
END;
END pkg_order;
-- 1. 用临时表替代包级变量
CREATE GLOBAL TEMPORARY TABLE pkg_context (
ctx_key VARCHAR(50),
ctx_value VARCHAR(200)
) ON COMMIT PRESERVE ROWS;
-- 2. 初始化过程
CREATE OR REPLACE PROCEDURE sp_init_batch AS
v_batch_id NUMBER;
BEGIN
SELECT seq_batch.NEXTVAL
INTO v_batch_id FROM DUAL;
-- 写入上下文临时表
DELETE FROM pkg_context;
INSERT INTO pkg_context VALUES
('batch_id', TO_CHAR(v_batch_id));
INSERT INTO pkg_context VALUES
('error_cnt', '0');
INSERT INTO batch_header
(batch_id, status, create_time)
VALUES (v_batch_id, 'OPEN', NOW());
END;
-- 3. 添加明细(通过上下文获取 batch_id)
CREATE OR REPLACE PROCEDURE sp_add_item(
p_product_id NUMBER,
p_qty NUMBER
) AS
v_batch_id NUMBER;
v_err_cnt NUMBER;
BEGIN
-- 从上下文读取
SELECT TO_NUMBER(ctx_value)
INTO v_batch_id
FROM pkg_context
WHERE ctx_key = 'batch_id';
INSERT INTO batch_detail
(batch_id, product_id, qty)
VALUES (v_batch_id, p_product_id, p_qty);
EXCEPTION
WHEN OTHERS THEN
-- 更新错误计数
UPDATE pkg_context
SET ctx_value =
TO_CHAR(TO_NUMBER(ctx_value)+1)
WHERE ctx_key = 'error_cnt';
END;
-- 4. 提交批次
CREATE OR REPLACE PROCEDURE sp_commit_batch AS
v_batch_id NUMBER;
v_err_cnt NUMBER;
BEGIN
SELECT ctx_value INTO v_batch_id
FROM pkg_context WHERE ctx_key='batch_id';
SELECT ctx_value INTO v_err_cnt
FROM pkg_context WHERE ctx_key='error_cnt';
UPDATE batch_header
SET status='DONE', item_count=v_err_cnt
WHERE batch_id = v_batch_id;
COMMIT;
END;
💡 经验总结:Oracle 的包级变量在会话内全局共享,达梦没有等价机制。解决方案有两种:① 用全局临时表做上下文存储;② 改为参数传递。前者更适合多过程协作的场景。
坑3: 关键词冲突——变量名撞车
现象:Oracle 存储过程中定义了变量 dateDiff、trace、rows 等,在达梦中编译报错——这些是达梦的保留关键字。
解决方案有两种:
-- 方案 A:手动用双引号包裹(不推荐,改动量大)
DECLARE
"dateDiff" NUMBER;
"trace" VARCHAR2(100);
BEGIN
"dateDiff" := 30;
END;
-- 方案 B:在 dm_svc.conf 中配置关键字屏蔽(推荐)
-- 找到达梦客户端配置文件 dm_svc.conf,添加:
KEYWORDS = #TRACE, #ROWS, #DATEDIFF
-- 配置后迁移和编译时,驱动会自动为这些关键字加双引号
💡 经验总结:迁移前先用达梦的关键字列表做一轮全局扫描,把所有冲突的变量名找出来。优先用 dm_svc.conf 配置屏蔽,比逐个修改变量名效率高得多。
坑4: 空字符串与 NULL 的爱恨纠葛
现象:用 DTS 迁移数据后,在达梦中用 WHERE phone IS NULL 查不到数据,但 WHERE phone = '' 也查不到。
根因:Oracle 中 ''(空字符串)等价于 NULL。达梦在 COMPATIBLE_MODE=0 时,'' 和 NULL 是不同的值。迁移时如果先导数据再改兼容模式,就会出现问题——
- 迁移时兼容模式 = 0,空字符串
''作为''存入 - 迁移后改兼容模式 = 2,新插入的
''自动转为NULL - 结果:同一列中既有
''又有NULL,两种查询条件都无法覆盖全部
-- 正确做法:在建库时就设好兼容模式
-- dm.ini 中配置:
COMPATIBLE_MODE = 2
-- 或者在创建实例后、建表前修改:
SP_SET_PARA_VALUE(2, 'COMPATIBLE_MODE', 2);
-- 需要重启数据库生效
-- 如果已经迁移了数据,修复方式:
UPDATE your_table SET phone = NULL WHERE phone = '';
💡 经验总结:COMPATIBLE_MODE 必须在创建实例后、建业务表之前设置好。迁移完成后再改,会导致空字符串和 NULL 混存的脏数据。
坑5: 系统包缺失——最耗时的逐一排查
| Oracle 系统包 | 达梦支持情况 | 替代方案 |
|---|---|---|
DBMS_OUTPUT.PUT_LINE |
支持 | 直接用,或用 PRINT |
DBMS_CRYPTO |
缺失 | 用 DBMS_OBFUSCATION_TOOLKIT 替代,或 Java 层实现加密 |
UTL_I18N |
缺失 | 自定义同名包,用 RAWTOHEX/HEXTORAW 实现 |
UTL_ENCODE.QUOTED_PRINTABLE_ENCODE |
缺失 | 用 BASE64_ENCODE 替代 |
UTL_SMTP |
需补丁 | 打补丁后执行 SP_CREATE_SYSTEM_PACKAGES(1, 'UTL_SMTP') |
DBMS_SQL |
部分支持 | 使用 describe_columns 前必须先 execute 游标 |
DBMS_LOB |
部分支持 | 部分方法可用,复杂操作需改写 |
以下是一个典型的 DBMS_CRYPTO 替代方案:
-- Oracle 原始代码(AES 加密)
FUNCTION encrypt_aes(input_string VARCHAR2,
key VARCHAR2)
RETURN VARCHAR2 IS
l_type PLS_INTEGER :=
dbms_crypto.encrypt_aes128 +
dbms_crypto.pad_pkcs5 +
dbms_crypto.chain_cbc;
l_encval RAW(2000);
BEGIN
l_encval := dbms_crypto.encrypt(
src => utl_i18n.string_to_raw(
input_string, 'AL32UTF8'),
typ => l_type,
key => utl_i18n.string_to_raw(
key, 'AL32UTF8')
);
RETURN l_encval;
END;
-- 达梦替代方案
FUNCTION encrypt_aes(input_string VARCHAR2,
key VARCHAR2)
RETURN VARCHAR2 IS
encrypted_string VARCHAR2(2048);
BEGIN
-- 使用达梦内置的加密工具包
DBMS_OBFUSCATION_TOOLKIT.DESENCRYPT(
input_string, key, encrypted_string
);
RETURN encrypted_string;
END;
坑6: CONNECT BY 递归查询改写
-- 部门层级查询
SELECT
department_id,
department_name,
parent_id,
LEVEL
FROM departments
START WITH parent_id IS NULL
CONNECT BY PRIOR department_id
= parent_id;
-- 含层级过滤
SELECT * FROM (
SELECT department_id,
department_name,
LEVEL AS lvl
FROM departments
START WITH parent_id IS NULL
CONNECT BY PRIOR department_id
= parent_id
) WHERE lvl <= 3;
-- 部门层级查询
WITH RECURSIVE dept_tree AS (
-- 锚点:根节点
SELECT department_id,
department_name,
parent_id,
1 AS level
FROM departments
WHERE parent_id IS NULL
UNION ALL
-- 递归:子节点
SELECT d.department_id,
d.department_name,
d.parent_id,
dt.level + 1
FROM departments d
INNER JOIN dept_tree dt
ON d.parent_id = dt.department_id
)
SELECT * FROM dept_tree;
-- 含层级过滤
WITH RECURSIVE dept_tree AS (
SELECT department_id,
department_name,
parent_id,
1 AS level
FROM departments
WHERE parent_id IS NULL
UNION ALL
SELECT d.department_id,
d.department_name,
d.parent_id,
dt.level + 1
FROM departments d
INNER JOIN dept_tree dt
ON d.parent_id = dt.department_id
WHERE dt.level < 3 -- 直接在这里限制
)
SELECT * FROM dept_tree;
💡 经验总结:CONNECT BY 的LEVEL伪列在达梦中需要手动用数字列模拟。另外,NOCYCLE关键字在达梦的递归 CTE 中不直接支持,需要通过条件判断防止死循环。
四、AnyLine 迁移方案
面对上述种种坑点,手动逐一改写和验证的效率很低。我们的解决思路是:利用 AnyLine 的动态数据源和元数据 API,构建一套自动化的迁移工具链,让框架来处理数据库差异,我们只关注业务逻辑的适配。
4.1 架构总览
DataSourceHolder.reg() 同时注册 Oracle 和达梦数据源,运行时动态切换
service.metadata() 分别采集两个库的表结构、存储过程、索引等元数据
4.2 双数据源并行——AnyLine 的核心优势
在迁移过程中,我们需要同时连接 Oracle(源库)和达梦(目标库),进行数据采集、对比验证等操作。AnyLine 的动态数据源注册机制让这件事变得非常简单:
// 同时注册 Oracle 和达梦数据源
// Oracle 源库
DataSourceHolder.reg(
"oracle_source", // 数据源 key
"oracle", // 数据库类型
"jdbc:oracle:thin:@ip:1521:orcl",
"user",
"password"
);
// 达梦目标库
DataSourceHolder.reg(
"dm_target", // 数据源 key
"dm", // 数据库类型(达梦)
"jdbc:dm://ip:5236",
"user",
"password"
);
// 查询 Oracle 中的存储过程列表
DataSet<DataRow> oracleProcs = ServiceProxy
.service("oracle_source")
.metadata()
.procedures();
// 查询达梦中已迁移的存储过程列表
DataSet<DataRow> dmProcs = ServiceProxy
.service("dm_target")
.metadata()
.procedures();
这就是 AnyLine 在迁移场景下的核心价值——同一套代码,通过 service(key) 切换不同的数据源,底层自动适配不同数据库的元数据查询语法。你不需要为 Oracle 写一套 ALL_PROCEDURES 查询,再为达梦写一套 ALL_PROCEDURES 查询,AnyLine 的 Adapter 帮你处理了这些差异。
4.3 元数据驱动的迁移流程
AnyLine 的元数据 API 提供了统一的 Table、Column、Index 等模型,屏蔽了不同数据库获取元数据的语法差异。迁移流程可以完全基于元数据驱动:
ServiceProxy.service("oracle_source").metadata().tables() 获取 Oracle 所有表结构
ServiceProxy.service("dm_target").ddl().create(table) 在达梦中建表
4.4 AnyLine Adapter 如何处理 SQL 语法差异
AnyLine 的核心设计哲学是:一套 API,多库通用。当你调用 service.select() 时,底层流程如下:
ServiceProxy.service("dm_target").select(configs, conditions)
这意味着,在迁移验证阶段,你可以用同一套查询代码同时跑 Oracle 和达梦,框架会自动生成各自数据库兼容的 SQL:
// 同一套代码,查询不同数据库
ConfigStore configs = new DefaultConfigStore();
configs.and("status", "ACTIVE");
configs.order("create_time DESC");
// Oracle 执行时自动生成:
// SELECT * FROM orders WHERE status = ?
// 并使用 Oracle 的分页语法
DataSet<DataRow> oracleData = ServiceProxy
.service("oracle_source")
.select("orders", configs);
// 达梦执行时自动生成:
// SELECT * FROM orders WHERE status = ?
// 并使用达梦的分页语法
DataSet<DataRow> dmData = ServiceProxy
.service("dm_target")
.select("orders", configs);
// 对比结果
log.info("Oracle 行数: {}, 达梦行数: {}",
oracleData.size(), dmData.size());
五、实战案例
案例1: 双数据源动态注册与切换
以下是完整的 Spring Boot 集成示例,展示如何在迁移工具中管理多个数据源:
@Service
public class MigrationManager {
private final AnylineService oracleService;
private final AnylineService dmService;
public MigrationManager() {
// 注册 Oracle 源数据源
DataSourceHolder.reg(
"oracle_prod",
"oracle",
"jdbc:oracle:thin:@ip:1521:orcl",
"app_user", "oracle_pass"
);
// 注册达梦目标数据源
DataSourceHolder.reg(
"dm_prod",
"dm",
"jdbc:dm://ip:5236?compatibleMode=oracle",
"APP_USER", "dm_pass"
);
}
/**
* 采集 Oracle 存储过程元数据
*/
public List<Procedure> collectOracleProcedures() {
ConfigStore configs = new DefaultConfigStore();
// 只查当前 schema 的存储过程
configs.add("OWNER", "APP_USER");
return oracleService
.metadata()
.procedures(configs);
}
/**
* 采集达梦已迁移的存储过程
*/
public List<Procedure> collectDMProcedures() {
ConfigStore configs = new DefaultConfigStore();
configs.add("OWNER", "APP_USER");
return dmService
.metadata()
.procedures(configs);
}
/**
* 生成迁移差异报告
*/
public void generateDiffReport() {
DataSet<DataRow> oracleProcs = collectOracleProcedures();
DataSet<DataRow> dmProcs = collectDMProcedures();
// 提取名称集合
Set<String> oracleNames = new HashSet<>();
for (DataRow row : oracleProcs) {
oracleNames.add(row.getString("PROCEDURE_NAME"));
}
Set<String> dmNames = new HashSet<>();
for (DataRow row : dmProcs) {
dmNames.add(row.getString("PROCEDURE_NAME"));
}
// 分析差异
Set<String> missing = new HashSet<>(oracleNames);
missing.removeAll(dmNames);
Set<String> extra = new HashSet<>(dmNames);
extra.removeAll(oracleNames);
log.info("=== 存储过程迁移差异报告 ===");
log.info("Oracle 总数: {}", oracleNames.size());
log.info("达梦已迁移: {}", dmNames.size());
log.info("待迁移: {} → {}", missing.size(), missing);
log.info("多余(需确认): {} → {}", extra.size(), extra);
}
}
案例2: 元数据对比——自动发现表结构差异
后来发现AnyLine实际已经提供了《对比数据库(表、列)之间的差异及成生DDL》/**
* 对比 Oracle 和达梦的表结构差异
* 利用 AnyLine 元数据 API 统一获取 Table 对象
*/
public class SchemaComparator {
private AnylineService oracleService;
private AnylineService dmService;
/**
* 逐表对比列定义
*/
public List<String> compareTableColumns(String tableName) {
List<String> diffs = new ArrayList<>();
// 获取 Oracle 表结构(struct 参数指定要查询的属性)
// struct=4 表示查询列信息
Table oracleTable = oracleService
.metadata()
.table(true, tableName, 4);
// 获取达梦表结构
Table dmTable = dmService
.metadata()
.table(true, tableName, 4);
if (oracleTable == null) {
diffs.add("Oracle 中不存在表: " + tableName);
return diffs;
}
if (dmTable == null) {
diffs.add("达梦中尚未创建表: " + tableName);
return diffs;
}
// 对比列
for (Column oraCol : oracleTable.getColumns()) {
String colName = oraCol.getName();
Column dmCol = dmTable.getColumn(colName);
if (dmCol == null) {
diffs.add(String.format(
"列 [%s] 在达梦中缺失", colName));
continue;
}
// 对比类型
String oraType = oraCol.getTypeName();
String dmType = dmCol.getTypeName();
if (!isTypeCompatible(oraType, dmType)) {
diffs.add(String.format(
"列 [%s] 类型不匹配: Oracle=%s, 达梦=%s",
colName, oraType, dmType));
}
// 对比长度
if (oraCol.getLength() != dmCol.getLength()) {
diffs.add(String.format(
"列 [%s] 长度不匹配: Oracle=%d, 达梦=%d",
colName, oraCol.getLength(), dmCol.getLength()));
}
// 对比可空
if (oraCol.isNullable() != dmCol.isNullable()) {
diffs.add(String.format(
"列 [%s] 可空性不匹配: Oracle=%b, 达梦=%b",
colName, oraCol.isNullable(), dmCol.isNullable()));
}
}
// 检查达梦中是否有多余的列
for (Column dmCol : dmTable.getColumns()) {
if (oracleTable.getColumn(dmCol.getName()) == null) {
diffs.add(String.format(
"列 [%s] 在 Oracle 中不存在(达梦多余)",
dmCol.getName()));
}
}
return diffs;
}
/**
* 类型兼容性检查
*/
private boolean isTypeCompatible(String oracleType, String dmType) {
// 定义类型映射关系
Map<String, Set<String>> compatible = Map.of(
"VARCHAR2", Set.of("VARCHAR", "VARCHAR2"),
"NUMBER", Set.of("DECIMAL", "NUMBER", "INT", "BIGINT"),
"DATE", Set.of("TIMESTAMP", "DATETIME"),
"CLOB", Set.of("TEXT", "CLOB"),
"BLOB", Set.of("BLOB", "BINARY"),
"RAW", Set.of("VARBINARY", "BINARY")
);
String baseOracle = oracleType.replaceAll("\\(.*\\)", "").trim().toUpperCase();
String baseDM = dmType.replaceAll("\\(.*\\)", "").trim().toUpperCase();
Set<String> allowed = compatible.get(baseOracle);
return allowed != null && allowed.contains(baseDM);
}
/**
* 批量对比所有表
*/
public Map<String, List<String>> compareAllTables() {
Map<String, List<String>> report = new LinkedHashMap<>();
// 获取 Oracle 所有表
DataSet<DataRow> tables = oracleService
.metadata()
.tables();
for (DataRow row : tables) {
String tableName = row.getString("TABLE_NAME");
List<String> diffs = compareTableColumns(tableName);
if (!diffs.isEmpty()) {
report.put(tableName, diffs);
}
}
log.info("=== 表结构差异报告 ===");
log.info("检查表数: {}", tables.size());
log.info("有差异的表: {}", report.size());
return report;
}
}
案例3: 存储过程自动化改写辅助
虽然 AnyLine 不能直接改写 PL/SQL 代码,但可以利用其元数据 API 提取存储过程的依赖关系,辅助人工改写:
/**
* 分析存储过程的依赖关系
* 帮助理清 PACKAGE 拆解的优先级
*/
public class ProcedureDependencyAnalyzer {
private AnylineService oracleService;
/**
* 提取存储过程引用的所有对象
*/
public Map<String, Set<String>> analyzeDependencies() {
Map<String, Set<String>> deps = new HashMap<>();
// 查询所有存储过程
DataSet<DataRow> procs = oracleService
.metadata()
.procedures();
for (DataRow proc : procs) {
String procName = proc.getString("PROCEDURE_NAME");
String procBody = proc.getString("BODY"); // 存储过程体
Set<String> referenced = new HashSet<>();
// 扫描存储过程体中的引用对象
// 这里简化处理,实际应该用 SQL 解析器
if (procBody != null) {
// 提取引用的表名
Pattern tablePattern = Pattern.compile(
"(?:FROM|INTO|UPDATE|JOIN)\\s+(\\w+)",
Pattern.CASE_INSENSITIVE);
Matcher m = tablePattern.matcher(procBody);
while (m.find()) {
referenced.add("TABLE:" + m.group(1).toUpperCase());
}
// 提取调用的其他存储过程
Pattern procPattern = Pattern.compile(
"(?:CALL|EXEC)\\s+(\\w+(?:\\.\\w+)?)",
Pattern.CASE_INSENSITIVE);
m = procPattern.matcher(procBody);
while (m.find()) {
referenced.add("PROC:" + m.group(1).toUpperCase());
}
// 提取使用的系统包
Pattern pkgPattern = Pattern.compile(
"(DBMS_\\w+|UTL_\\w+)\\.",
Pattern.CASE_INSENSITIVE);
m = pkgPattern.matcher(procBody);
while (m.find()) {
referenced.add("PKG:" + m.group(1).toUpperCase());
}
}
deps.put(procName, referenced);
}
return deps;
}
/**
* 生成 PACKAGE 拆解建议
*/
public void generateSplitAdvice(
Map<String, Set<String>> deps) {
// 按系统包依赖分组
Map<String, List<String>> pkgDepGroups = new HashMap<>();
for (var entry : deps.entrySet()) {
for (String ref : entry.getValue()) {
if (ref.startsWith("PKG:")) {
pkgDepGroups
.computeIfAbsent(ref, k -> new ArrayList<>())
.add(entry.getKey());
}
}
}
log.info("=== PACKAGE 拆解建议 ===");
for (var entry : pkgDepGroups.entrySet()) {
log.info("依赖 {} 的过程({}个): {}",
entry.getKey(),
entry.getValue().size(),
entry.getValue());
if (entry.getKey().contains("DBMS_CRYPTO") ||
entry.getKey().contains("UTL_I18N")) {
log.warn(" ⚠️ 该包在达梦中缺失,需要替代方案");
}
}
}
}
案例4: 数据一致性校验
迁移完成后,最关键的一步是数据校验。利用 AnyLine 的双数据源能力,可以方便地做跨库比对:
/**
* 跨数据源数据校验
* 同时查询 Oracle 和达梦,对比结果集
*/
public class DataVerifier {
private AnylineService oracleService;
private AnylineService dmService;
/**
* 行数校验:快速检查每张表的数据量是否一致
*/
public Map<String, long[]> verifyRowCount() {
Map<String, long[]> result = new LinkedHashMap<>();
// 获取 Oracle 所有用户表
DataSet<DataRow> tables = oracleService
.metadata()
.tables();
for (DataRow row : tables) {
String tableName = row.getString("TABLE_NAME");
// Oracle 行数
ConfigStore countConfigs = new DefaultConfigStore();
long oracleCount = oracleService
.count(tableName, countConfigs);
// 达梦行数
long dmCount = dmService
.count(tableName, countConfigs);
result.put(tableName, new long[]{oracleCount, dmCount});
if (oracleCount != dmCount) {
log.error("行数不一致: {} Oracle={} 达梦={}",
tableName, oracleCount, dmCount);
}
}
return result;
}
/**
* 抽样校验:随机抽取 N 行做全字段对比
*/
public List<String> spotCheck(String tableName, int sampleSize) {
List<String> diffs = new ArrayList<>();
// 从 Oracle 随机抽样
DataSet<DataRow> oracleSamples = oracleService
.selects(tableName,
new DefaultConfigStore()
.order("DBMS_RANDOM.VALUE")
.limit(sampleSize));
// 根据主键在达梦中查找对应行
Table meta = dmService.metadata().table(true, tableName, 4);
List<String> primaryKeys = meta.getPrimaryKeys();
for (DataRow oraRow : oracleSamples) {
ConfigStore query = new DefaultConfigStore();
for (String pk : primaryKeys) {
query.and(pk, oraRow.get(pk));
}
DataSet<DataRow> dmRows = dmService.select(tableName, query);
if (dmRows.isEmpty()) {
diffs.add("主键 " + primaryKeys + " = " +
primaryKeys.stream()
.map(k -> oraRow.get(k))
.toList() +
" 在达梦中不存在");
continue;
}
DataRow dmRow = dmRows.get(0);
// 逐字段对比
for (Column col : meta.getColumns()) {
String colName = col.getName();
Object oraVal = oraRow.get(colName);
Object dmVal = dmRow.get(colName);
if (!Objects.equals(
String.valueOf(oraVal),
String.valueOf(dmVal))) {
diffs.add(String.format(
"表 %s 字段 %s 值不一致: Oracle=%s, 达梦=%s",
tableName, colName, oraVal, dmVal));
}
}
}
return diffs;
}
}
案例5: 批量迁移编排——完整的迁移控制器
/**
* 迁移编排控制器
* 串联整个迁移流程
*/
@RestController
@RequestMapping("/migration")
public class MigrationController {
@Autowired private MigrationManager migrationManager;
@Autowired private SchemaComparator schemaComparator;
@Autowired private DataVerifier dataVerifier;
/**
* Step 1: 迁移前评估
*/
@GetMapping("/assess")
public Map<String, Object> assess() {
Map<String, Object> report = new LinkedHashMap<>();
// 存储过程差异
migrationManager.generateDiffReport();
report.put("step", "assess");
report.put("status", "completed");
// 表结构差异
Map<String, List<String>> schemaDiffs =
schemaComparator.compareAllTables();
report.put("schemaDiffs", schemaDiffs);
report.put("schemaDiffCount", schemaDiffs.size());
return report;
}
/**
* Step 2: 表结构迁移
* 从 Oracle 读取表结构,在达梦中创建
*/
@PostMapping("/migrate-schema/{tableName}")
public Map<String, Object> migrateSchema(
@PathVariable String tableName) {
Map<String, Object> result = new LinkedHashMap<>();
AnylineService oracleService = ServiceProxy.service("oracle_prod");
AnylineService dmService = ServiceProxy.service("dm_prod");
// 从 Oracle 获取完整表结构
Table table = oracleService
.metadata()
.table(true, tableName, true); // 全属性
if (table == null) {
result.put("status", "error");
result.put("message", "Oracle 中不存在表: " + tableName);
return result;
}
// 在达梦中创建表
boolean success = dmService.ddl().create(table);
result.put("status", success ? "success" : "failed");
result.put("table", tableName);
result.put("columns", table.getColumns().size());
// 创建索引
for (Index idx : table.getIndexs()) {
dmService.ddl().add(idx);
}
return result;
}
/**
* 类型转换逻辑
*/
private void convertColumnType(Column col) {
String type = col.getTypeName().toUpperCase();
//这一步TypeMetadata也提供了数据类型映射
}
/**
* Step 3: 数据校验
*/
@GetMapping("/verify")
public Map<String, Object> verify() {
Map<String, Object> result = new LinkedHashMap<>();
// 行数校验
Map<String, long[]> rowCountResult =
dataVerifier.verifyRowCount();
result.put("rowCheck", rowCountResult);
// 统计不一致的表
long mismatchCount = rowCountResult.values().stream()
.filter(v -> v[0] != v[1])
.count();
result.put("mismatchTables", mismatchCount);
result.put("totalTables", rowCountResult.size());
result.put("status",
mismatchCount == 0 ? "ALL_PASSED" : "HAS_ERRORS");
return result;
}
}
六、迁移检查清单
基于我们 6 周的迁移经验,整理了一份完整的检查清单:
迁移前
| 检查项 | 具体内容 | 状态 |
|---|---|---|
| ✅ 兼容模式 | 创建达梦实例时就设好 COMPATIBLE_MODE=2,在建业务表之前 | 必须 |
| ✅ 关键字扫描 | 用达梦关键字列表扫描 Oracle 存储过程中的变量名、表别名 | 必须 |
| ✅ DATE 字段排查 | 全局搜索所有 DATE 类型字段,统一改为 TIMESTAMP | 必须 |
| ✅ 系统包清单 | 列出所有使用的 Oracle 系统包,确认达梦是否支持 | 必须 |
| ✅ JDBC 驱动版本 | 确认 Oracle 版本对应的 JDBC 驱动 jar 包,DTS 中手动指定 | 建议 |
| ✅ 迁移顺序 | 先序列 → 再表 → 最后视图/函数/存储过程/包 | 必须 |
迁移中
| 检查项 | 具体内容 | 状态 |
|---|---|---|
| ⚠️ PACKAGE 拆解 | 理清包级变量 → 用临时表或参数传递替代 | 必须 |
| ⚠️ DDL+DML 混用 | 同一存储过程中,DML 全部改为 EXECUTE IMMEDIATE 动态执行 | 必须 |
| ⚠️ CONNECT BY | 替换为 WITH RECURSIVE CTE,注意 LEVEL 列的手动维护 | 按需 |
| ⚠️ CURSOR 子查询 | 达梦不支持 CURSOR(...) 作为查询项,需在应用层拆分 | 按需 |
| ⚠️ 变量赋值 | 所有 SET var = ... 改为 var := ... | 简单 |
| ⚠️ 表别名 AS | 删除表别名前的 AS 关键字 | 简单 |
迁移后
| 检查项 | 具体内容 | 状态 |
|---|---|---|
| ✅ 行数校验 | 每张表的 Oracle 行数 = 达梦行数 | 必须 |
| ✅ 抽样校验 | 随机抽取关键表数据,逐字段对比 | 必须 |
| ✅ 存储过程功能测试 | 逐个调用存储过程,验证业务逻辑正确 | 必须 |
| ✅ 性能基准测试 | 对比关键 SQL 在 Oracle 和达梦中的执行时间 | 建议 |
| ✅ 空字符串验证 | 检查 IS NULL 和 = '' 的查询结果是否符合预期 | 建议 |
| ✅ 并发测试 | 验证事务隔离级别差异(达梦默认 READ COMMITTED) | 建议 |
七、总结与经验
经过 6 周的迁移实战,我们总结了几条核心经验:
COMPATIBLE_MODE=2 不是"出了问题再开"的配置项,而是在建库时就要决定的基础设定。事后修改会导致空字符串/NULL 的混存,修复成本极高。
不要试图用正则替换来批量改写存储过程。正确的做法是:先用工具分析依赖关系 → 按依赖分组 → 按优先级逐个改写 → 逐个测试。一个 87 个存储过程的项目,光存储过程改写就花了 3 周。
AnyLine 的动态数据源 + 元数据 API 在迁移场景下发挥巨大价值。通过 service(key) 在同一套代码中切换 Oracle 和达梦,利用 metadata() 统一获取表结构,用 ddl() 自动建表建索引。这让我们的元数据采集、对比校验工作效率提升了 5 倍以上。
行数一致不代表数据正确。我们遇到过 NUMBER 精度丢失导致金额差了 0.01 的情况,也遇到过 DATE 改 TIMESTAMP 后时间部分全部变成 00:00:00 的情况。一定要做抽样全字段校验。
项目完成
87 个存储过程全部迁移成功,23 个 PACKAGE 拆解完毕,数据校验通过率 100%。
AnyLine 贡献
元数据采集、对比校验、双数据源管理等环节节省约 70% 工作量。
最大教训
COMPATIBLE_MODE 设错顺序导致数据修复花费了额外 2 天。切记:建库时就设好!
最大挑战
PACKAGE 中的包级变量和 DBMS_CRYPTO 加密逻辑,各花了近一周解决。
信创改造不是一次性的技术迁移,而是一次架构思维的转变。从依赖 Oracle 的专有特性,到拥抱数据库无关的抽象层——AnyLine 提供的正是这种抽象能力。当你的代码不再绑定某一种数据库语法时,"去 O" 就不再是一场噩梦,而是一次自然的架构演进。