信创改造实战:用 AnyLine 将 Oracle 存储过程迁移到达梦的踩坑实录

一套基于 AnyLine 动态数据源 + 元数据驱动的 Oracle → 达梦存储过程迁移方法论,附完整代码与真实踩坑记录

🏷️ 信创改造 🏷️ 达梦DM8 🏷️ AnyLine 📖 阅读约 20 分钟
87
存储过程总数
23
需要手动改写的包
14
踩过的典型坑
6 周
完整迁移周期

一、信创改造背景

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 无直接对应 需要完全重写文件操作逻辑
⚠️ 最大陷阱:DATE 类型

很多迁移项目在这上面栽了跟头。Oracle 的 DATE 类型包含世纪、年、月、日、时、分、秒,而达梦的 DATE 只包含年月日。迁移前必须全局排查所有 DATE 字段,统一改为 TIMESTAMP,否则时间精度丢失导致的业务逻辑错误极难排查。

2.2 语法差异——魔鬼在细节

🔴 Oracle 原始写法
-- 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 和 := 都支持仅支持 :=★☆☆
表别名 ASSELECT * FROM emp AS e表别名不能用 AS,列别名可以★☆☆
存储过程调用call 可省略必须使用 CALL 关键字★☆☆
CURSOR 子查询SELECT CURSOR(...) FROM ...不支持,需在应用层拆分★★★

2.3 存储过程改造——工作量最大、风险最高

存储过程改造是整个迁移工作中工作量最大、风险最高的环节。难点集中在以下几个方面:

🔥 存储过程改造的核心难点
  1. PACKAGE 拆解:一个包体里几十个过程和函数互相调用,还有包级变量,需要理清依赖关系后逐个拆分
  2. 动态 SQL 适配EXECUTE IMMEDIATE 在达梦中行为有差异,特别是绑定变量和返回值的处理
  3. 系统包替代DBMS_CRYPTOUTL_I18NUTL_ENCODE 等包在达梦中部分缺失或功能不全
  4. DDL + DML 混用:存储过程中先执行 DDL(如创建索引)再执行 DML(如 INSERT),达梦会报"对象定义被修改"
  5. 隐式类型转换:Oracle 的大量隐式转换在达梦中可能失效或产生不同结果

三、真实踩坑实录

以下是我们在实际项目中遇到的典型问题,每一个都花了至少半天到一周的时间来解决。希望这些经验能帮你少走弯路。

坑1: "对象定义被修改"——DDL 和 DML 不能混用

💥
存储过程执行报错:对象定义被修改
严重 耗时 3 天

现象:存储过程编译可以通过,但执行时报错"对象定义被修改"。排查发现,过程中先对一张表执行了 DROP INDEX(DDL),然后又执行了 INSERT(DML)。

根因:达梦数据库会预先获取 SQL 的所有执行计划再执行。当过程中先修改了表定义(DDL),字典信息发生变化,后续 DML 的执行计划与实际不一致,就触发了这个错误。

🔴 Oracle 中可以这样写
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;
🟢 达梦需要 DML 改动态执行
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 拆解——包级变量是最大的坑

💥
PACKAGE 拆解后逻辑错乱
严重 耗时 5 天

现象:Oracle 中有一个 PKG_ORDER 包,包含 12 个存储过程和 8 个函数,其中有一个包级变量 g_batch_id 被多个过程共享。直接拆分成独立存储过程后,业务逻辑断裂。

🔴 Oracle PACKAGE 结构
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: 关键词冲突——变量名撞车

变量名是达梦保留关键字
中等 耗时 1 天

现象:Oracle 存储过程中定义了变量 dateDifftracerows 等,在达梦中编译报错——这些是达梦的保留关键字。

解决方案有两种:

-- 方案 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 的爱恨纠葛

COMPATIBLE_MODE 设错顺序导致数据不一致
严重 耗时 2 天

现象:用 DTS 迁移数据后,在达梦中用 WHERE phone IS NULL 查不到数据,但 WHERE phone = '' 也查不到。

根因:Oracle 中 ''(空字符串)等价于 NULL。达梦在 COMPATIBLE_MODE=0 时,''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: 系统包缺失——最耗时的逐一排查

💥
DBMS_CRYPTO / UTL_I18N / UTL_ENCODE 不可用
严重 耗时 1 周
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 递归查询改写

树形查询语法完全不同
中等 耗时 2 天
🔴 Oracle 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
-- 部门层级查询
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 架构总览

迁移平台 基于 AnyLine 构建 Oracle → 达梦迁移工具
双数据源 通过 DataSourceHolder.reg() 同时注册 Oracle 和达梦数据源,运行时动态切换
元数据采集 通过 service.metadata() 分别采集两个库的表结构、存储过程、索引等元数据
差异分析 对比 Oracle 和达梦的元数据差异,生成迁移清单和改写建议
SQL 改写 利用 AnyLine 的 Adapter 层自动生成目标库兼容的 SQL 语法
数据校验 跨数据源对比数据一致性,输出迁移报告

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 提供了统一的 TableColumnIndex 等模型,屏蔽了不同数据库获取元数据的语法差异。迁移流程可以完全基于元数据驱动:

Step 1 采集源库元数据:通过 ServiceProxy.service("oracle_source").metadata().tables() 获取 Oracle 所有表结构
Step 2 类型映射转换:遍历每个 Table 的 Column 列表,根据类型映射表将 Oracle 类型转换为达梦类型
Step 3 目标库 DDL 生成:修改 Table 对象的属性后,通过 ServiceProxy.service("dm_target").ddl().create(table) 在达梦中建表
Step 4 数据迁移 + 校验:通过 AnyLine 的 select/insert 在两个数据源间搬运数据,并逐行校验

4.4 AnyLine Adapter 如何处理 SQL 语法差异

AnyLine 的核心设计哲学是:一套 API,多库通用。当你调用 service.select() 时,底层流程如下:

入口 ServiceProxy.service("dm_target").select(configs, conditions)
DAO 路由 根据数据源 key 定位到对应的 Dataservice → 找到 DMAdapter
Adapter 生成命令 DMAdapter.buildSelectRun() 生成达梦兼容的 SQL(如用 LIMIT 替代 ROWNUM)
JDBCActuator 执行 JDBCActuator 调用达梦 JDBC 驱动执行 SQL 并封装结果
返回 DataSet<DataRow> 统一的动态数据结构

这意味着,在迁移验证阶段,你可以用同一套查询代码同时跑 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 是迁移利器

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" 就不再是一场噩梦,而是一次自然的架构演进。

📚 相关资源

AnyLine Gitee 仓库 | AnyLine 官方文档 | 达梦官方文档

本文基于实际信创项目经验撰写,代码基于 AnyLine 开源框架 API。