以下是完整的 GBase 8s CLOB → VARCHAR(32739) 数据迁移方案文档,代码已根据实际运行验证修正,可直接交付客户使用。
GBase 8s CLOB 转 VARCHAR(32739) 数据迁移方案
一、方案概述
1.1 背景
客户表 t_da_attach_log 的 op_content 字段为 CLOB 类型,存在性能问题,需迁移到新表 t_da_attach_log_new,将字段改为 VARCHAR(32739)。数据量约 800 万行。
1.2 核心设计
表格
| 问题 | 解决方案 |
|---|---|
| CLOB 全表扫描内存暴涨 | JDBC 服务器端游标 :setFetchSize() 流式读取,客户端只缓存一批数据 |
| UUID 主键分页慢 | 不用分页。顺序游标遍历,天然无 OFFSET 性能衰减问题 |
| 逻辑日志压力大 | 分批提交 :每 COMMIT_EVERY 行(如 5000 行)执行一次 commit() |
| CLOB 转 VARCHAR(32739) | Java 层截断 :Clob.getSubString(1, 32739),不依赖数据库端函数 |
| LOB 资源泄漏 | 连接串 AUTO_FREE_LOB=1;lobcache=-1 + 代码显式 clob.free() |
1.3 为什么不使用 ROWID / LIMIT 分页?
表格
| 方案 | 缺点 | 结论 |
|---|---|---|
ROWID 范围扫描 |
ROWID 非严格连续,分片表/删除后产生空洞,导致数据遗漏或重复 | ❌ 不推荐 |
ORDER BY id LIMIT n OFFSET m |
UUID 无序,OFFSET 越大全表扫描越慢,800万行后期极慢 | ❌ 不推荐 |
| JDBC 游标流式读取 | 无分页逻辑,内存稳定,速度均匀,兼容所有表类型 | ✅ 采用 |
二、数据库准备
在 testdb 库中执行以下 SQL,创建原表 和目标表:
sql
-- ============================================
-- 1. 创建原表(含 CLOB)
-- ============================================
DROP TABLE IF EXISTS t_da_attach_log;
CREATE TABLE t_da_attach_log (
id VARCHAR(32) NOT NULL,
archive_id VARCHAR(32),
op_user_id VARCHAR(36),
op_date DATETIME YEAR TO FRACTION(5),
op_content CLOB, -- 原字段:CLOB
op_user_name VARCHAR(100),
op_flag VARCHAR(100),
opinion VARCHAR(500),
log_type VARCHAR(10),
node_name VARCHAR(50),
PRIMARY KEY (id)
);
-- ============================================
-- 2. 创建目标表(CLOB 改为 VARCHAR(32739))
-- ============================================
DROP TABLE IF EXISTS t_da_attach_log_new;
CREATE TABLE t_da_attach_log_new (
id VARCHAR(32) NOT NULL,
archive_id VARCHAR(32),
op_user_id VARCHAR(36),
op_date DATETIME YEAR TO FRACTION(5),
op_content VARCHAR(32739), -- 新字段:VARCHAR(32739)
op_user_name VARCHAR(100),
op_flag VARCHAR(100),
opinion VARCHAR(500),
log_type VARCHAR(10),
node_name VARCHAR(50),
PRIMARY KEY (id)
);
三、Java 代码
将以下 3 个文件保存到同一目录(如 /root/clob_to_varchar/)。
3.1 DbConfig.java --- 数据库配置
java
/**
* 数据库连接配置常量类
* 根据实际环境修改以下参数
*/
public class DbConfig {
/**
* GBase 8s JDBC URL
* AUTO_FREE_LOB=1 : 自动释放 LOB,防止内存泄漏
* lobcache=-1 : 将 LOB 缓存到内存
* ifx_lock_mode_wait=60 : 锁等待 60 秒
*/
public static final String URL =
"jdbc:gbasedbt-sqli://192.168.0.11:9088/testdb:" +
"GBASEDBTSERVER=gbase01;" +
"AUTO_FREE_LOB=1;" +
"lobcache=-1;" +
"ifx_lock_mode_wait=60";
/** 数据库用户名 */
public static final String USER = "gbasedbt";
/** 数据库密码 */
public static final String PASSWORD = "GBase@20260410";
/** JDBC 驱动类名(GBase 8s 兼容 Informix 驱动) */
public static final String DRIVER_CLASS = "com.gbasedbt.jdbc.Driver";
// ==================== 迁移参数配置 ====================
/**
* 游标每次从服务器读取的行数。
* 值越大网络往返越少,但服务端游标占用资源越多。
* 800万数据量建议 5000 ~ 10000。
*/
public static final int FETCH_SIZE = 5000;
/**
* JDBC 批量插入的批次大小。
* 每积累 BATCH_SIZE 行执行一次 executeBatch()。
*/
public static final int BATCH_SIZE = 1000;
/**
* 每处理多少行执行一次 commit()。
* 控制逻辑日志增长,避免单事务过大。
* 必须是 BATCH_SIZE 的整数倍。
*/
public static final int COMMIT_EVERY = 5000;
/**
* CLOB 转 VARCHAR 时的最大字符长度。
* GBase 8s VARCHAR 最大 32765,这里取 32739 留余量。
*/
public static final int CLOB_MAX_LENGTH = 32739;
private DbConfig() {}
}
3.2 TestDataGenerator.java --- 测试数据生成器
java
import java.sql.*;
import java.util.UUID;
/**
* 测试数据生成器
* 向原表 t_da_attach_log 插入测试数据,用于本地验证迁移程序
*
* 运行前请确保:
* 1. 已执行建表 SQL
* 2. 已放置 GBase 8s JDBC 驱动 jar 到 classpath
*/
public class TestDataGenerator {
public static void main(String[] args) {
// 要生成的测试数据条数(本地验证建议 1万条,压力测试可改大)
final int TOTAL_ROWS = 10000;
try {
Class.forName(DbConfig.DRIVER_CLASS);
} catch (ClassNotFoundException e) {
System.err.println("❌ 找不到 JDBC 驱动,请将 gbasedbt jdbc jar 加入 classpath");
e.printStackTrace();
return;
}
String insertSql =
"INSERT INTO t_da_attach_log " +
"(id, archive_id, op_user_id, op_date, op_content, " +
" op_user_name, op_flag, opinion, log_type, node_name) " +
"VALUES (?, ?, ?, CURRENT YEAR TO FRACTION(5), ?, ?, ?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD)) {
// 关闭自动提交,手动控制事务
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(insertSql)) {
long startTime = System.currentTimeMillis();
for (int i = 1; i <= TOTAL_ROWS; i++) {
// 生成 UUID 作为主键(去横线,32位)
String id = UUID.randomUUID().toString().replace("-", "");
String archiveId = UUID.randomUUID().toString().replace("-", "");
String opUserId = UUID.randomUUID().toString();
// 构造 CLOB 内容:模拟不同长度的文本
String clobContent = buildClobContent(i);
ps.setString(1, id);
ps.setString(2, archiveId);
ps.setString(3, opUserId);
ps.setString(4, clobContent); // CLOB 字段,直接传 String,驱动自动处理
ps.setString(5, "用户_" + (i % 100));
ps.setString(6, i % 2 == 0 ? "新增档案" : "修改档案");
ps.setString(7, i % 3 == 0 ? "同意" : "");
ps.setString(8, i % 5 == 0 ? "XZDA" : "BGDA");
ps.setString(9, i % 7 == 0 ? "审批节点" : null);
ps.addBatch();
// 每 1000 条提交一次,避免事务过大
if (i % 1000 == 0) {
ps.executeBatch();
conn.commit();
System.out.printf("⏳ 已插入 %d / %d 条,耗时 %.1f 秒%n",
i, TOTAL_ROWS, (System.currentTimeMillis() - startTime) / 1000.0);
}
}
// 提交剩余批次
ps.executeBatch();
conn.commit();
long totalTime = System.currentTimeMillis() - startTime;
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.printf("✅ 测试数据生成完成!共插入 %,d 条,总耗时 %.2f 秒%n",
TOTAL_ROWS, totalTime / 1000.0);
} catch (SQLException e) {
conn.rollback();
throw e;
}
} catch (SQLException e) {
System.err.println("❌ 数据库操作失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 构造模拟 CLOB 内容
* 部分记录短文本,部分记录长文本(接近/超过 32K 边界),验证截断逻辑
*/
private static String buildClobContent(int index) {
StringBuilder sb = new StringBuilder();
sb.append("【操作记录第").append(index).append("条】");
// 模拟不同长度:
// 50% 短文本 (100~300 字符)
// 30% 中等文本 (1000~6000 字符)
// 20% 长文本 (30000~35000 字符,会触发截断)
int length;
if (index % 10 < 5) {
length = 100 + (index % 200);
} else if (index % 10 < 8) {
length = 1000 + (index % 5000);
} else {
length = 30000 + (index % 5000);
}
// 填充内容
String template = "这是一段模拟的档案操作日志内容,用于测试CLOB字段迁移到VARCHAR(32739)的兼容性。";
while (sb.length() < length) {
sb.append(template);
}
return sb.toString();
}
}
3.3 ClobToVarcharMigration.java --- 核心迁移程序
java
import java.sql.*;
/**
* GBase 8s CLOB → VARCHAR(32739) 数据迁移程序
*
* 核心机制:
* 1. 【流式读取】使用 Statement.setFetchSize() 启用服务器端游标,
* 避免一次性将 800万行加载到客户端内存。
* 2. 【Java层截断】不在SQL层用 dbms_lob_substr(GBase 8s默认无此Oracle函数),
* 而是在Java层通过 JDBC Clob.getSubString() 读取并截断为 32K 内文本,
* 兼容性最好,零依赖,不依赖任何数据库扩展包。
* 3. 【批量插入】使用 PreparedStatement.addBatch() 积累数据,减少网络往返。
* 4. 【分批提交】每 COMMIT_EVERY 行执行 commit(),控制逻辑日志大小。
*
* 优势:
* - 不依赖 LIMIT/OFFSET,无分页越往后越慢的问题
* - 不依赖 ROWID,兼容分片表和普通表
* - 不依赖 UUID 主键顺序
* - 内存占用稳定(只缓存 FETCH_SIZE + BATCH_SIZE 行)
*/
public class ClobToVarcharMigration {
// 统计计数器
private long totalRead = 0; // 已读取行数
private long totalInsert = 0; // 已插入行数
private long totalCommit = 0; // 已提交次数
private long totalTruncate = 0; // CLOB 被截断的行数
public static void main(String[] args) {
new ClobToVarcharMigration().run();
}
public void run() {
// 加载 JDBC 驱动
try {
Class.forName(DbConfig.DRIVER_CLASS);
System.out.println("✅ JDBC 驱动加载成功: " + DbConfig.DRIVER_CLASS);
} catch (ClassNotFoundException e) {
System.err.println("❌ 找不到 JDBC 驱动,请将 gbasedbt jdbc jar 加入 classpath");
e.printStackTrace();
return;
}
long startTime = System.currentTimeMillis();
// ============================================
// 源表查询 SQL:直接读取 CLOB,不在 SQL 层截断
// ============================================
// 不使用 dbms_lob_substr()(Oracle函数,GBase 8s默认无此函数)
// 截断逻辑放到 Java 层处理,兼容性最好
String selectSql =
"SELECT " +
" id, " +
" archive_id, " +
" op_user_id, " +
" op_date, " +
" op_content, " + // 直接读取 CLOB,不在 SQL 层截断
" op_user_name, " +
" op_flag, " +
" opinion, " +
" log_type, " +
" node_name " +
"FROM t_da_attach_log";
// 目标表插入 SQL
String insertSql =
"INSERT INTO t_da_attach_log_new " +
"(id, archive_id, op_user_id, op_date, op_content, " +
" op_user_name, op_flag, opinion, log_type, node_name) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
try (Connection readConn = DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD);
Connection writeConn = DriverManager.getConnection(DbConfig.URL, DbConfig.USER, DbConfig.PASSWORD)) {
// ========== 读取连接配置:启用服务器端游标 ==========
// 关键:setAutoCommit(false) + TYPE_FORWARD_ONLY + CONCUR_READ_ONLY
// 配合 setFetchSize() 启用游标,实现流式读取
readConn.setAutoCommit(false);
writeConn.setAutoCommit(false);
System.out.println("🔌 数据库连接建立成功");
System.out.printf("📋 配置参数: FETCH_SIZE=%d, BATCH_SIZE=%d, COMMIT_EVERY=%d%n",
DbConfig.FETCH_SIZE, DbConfig.BATCH_SIZE, DbConfig.COMMIT_EVERY);
System.out.println("⏳ 开始迁移数据,请耐心等待...");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
// 创建读取 Statement,启用游标
try (Statement readStmt = readConn.createStatement(
ResultSet.TYPE_FORWARD_ONLY, // 只向前游标,内存友好
ResultSet.CONCUR_READ_ONLY); // 只读模式
PreparedStatement insertPs = writeConn.prepareStatement(insertSql)) {
// 【核心】设置游标每次获取的行数,启用服务器端游标
// 对于 Informix/GBase 8s 驱动,fetchSize > 0 时会使用游标而非全量缓存
readStmt.setFetchSize(DbConfig.FETCH_SIZE);
// 执行查询
try (ResultSet rs = readStmt.executeQuery(selectSql)) {
int batchCounter = 0; // 当前批次计数
int commitCounter = 0; // 当前事务内行数计数
while (rs.next()) {
totalRead++;
// ========== 从源表读取数据 ==========
String id = rs.getString("id");
String archiveId = rs.getString("archive_id");
String opUserId = rs.getString("op_user_id");
Timestamp opDate = rs.getTimestamp("op_date");
// 【关键】通过 JDBC Clob 接口读取,只取前 CLOB_MAX_LENGTH 个字符
// 配合连接串 AUTO_FREE_LOB=1;lobcache=-1,读取后自动释放 LOB 资源
String opContent = readClobTruncated(rs, "op_content");
String opUserName = rs.getString("op_user_name");
String opFlag = rs.getString("op_flag");
String opinion = rs.getString("opinion");
String logType = rs.getString("log_type");
String nodeName = rs.getString("node_name");
// ========== 设置插入参数 ==========
insertPs.setString(1, id);
insertPs.setString(2, archiveId);
insertPs.setString(3, opUserId);
insertPs.setTimestamp(4, opDate);
insertPs.setString(5, opContent); // VARCHAR(32739) 字段
insertPs.setString(6, opUserName);
insertPs.setString(7, opFlag);
insertPs.setString(8, opinion);
insertPs.setString(9, logType);
insertPs.setString(10, nodeName);
insertPs.addBatch();
batchCounter++;
commitCounter++;
// ========== 批量执行插入 ==========
if (batchCounter >= DbConfig.BATCH_SIZE) {
insertPs.executeBatch();
insertPs.clearBatch();
totalInsert += batchCounter;
batchCounter = 0;
}
// ========== 分批提交(控制逻辑日志) ==========
if (commitCounter >= DbConfig.COMMIT_EVERY) {
writeConn.commit();
totalCommit++;
commitCounter = 0;
// 打印进度
printProgress(startTime);
}
}
// ========== 处理剩余未提交的批次 ==========
if (batchCounter > 0) {
insertPs.executeBatch();
totalInsert += batchCounter;
}
if (commitCounter > 0) {
writeConn.commit();
totalCommit++;
}
} // ResultSet 自动关闭,游标释放
} // Statement 自动关闭
// ========== 迁移完成,打印统计 ==========
long totalTime = System.currentTimeMillis() - startTime;
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("✅ 数据迁移完成!");
System.out.printf("📊 总读取行数: %,d%n", totalRead);
System.out.printf("📊 总插入行数: %,d%n", totalInsert);
System.out.printf("📊 总提交次数: %,d%n", totalCommit);
System.out.printf("📊 CLOB 截断行数: %,d%n", totalTruncate);
System.out.printf("⏱️ 总耗时: %.2f 秒 (%.2f 分钟)%n",
totalTime / 1000.0, totalTime / 60000.0);
System.out.printf("⚡ 平均速度: %.1f 行/秒%n",
totalRead * 1000.0 / totalTime);
// 数据校验:对比源表和目标表行数
verifyData(readConn);
} catch (SQLException e) {
System.err.println("❌ 迁移过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 安全读取 CLOB 并截断
*
* 逻辑:
* 1. 通过 rs.getClob() 获取 Clob 对象(不加载全部内容到内存)
* 2. 获取 CLOB 总长度,计算实际读取长度(取 min(长度, CLOB_MAX_LENGTH))
* 3. 使用 getSubString(1, readLength) 精确读取指定长度
* 4. 显式调用 clob.free() 释放资源(双保险:AUTO_FREE_LOB + 手动释放)
*
* @param rs ResultSet
* @param columnName CLOB 字段名
* @return 截断后的 String(最大 CLOB_MAX_LENGTH 字符),null 如果原值为 null
*/
private String readClobTruncated(ResultSet rs, String columnName) throws SQLException {
Clob clob = null;
try {
// 获取 Clob 对象
clob = rs.getClob(columnName);
if (clob == null) {
return null;
}
// 获取 CLOB 总长度
long clobLength = clob.length();
// 如果长度超过限制,标记截断计数
if (clobLength > DbConfig.CLOB_MAX_LENGTH) {
totalTruncate++;
}
// 计算实际读取长度(取较小值)
int readLength = (int) Math.min(clobLength, DbConfig.CLOB_MAX_LENGTH);
// 从第 1 个字符开始,读取 readLength 个字符
// 注意:getSubString 的索引从 1 开始(符合 SQL 标准)
String result = clob.getSubString(1, readLength);
return result;
} finally {
// 显式释放 CLOB 资源(即使 AUTO_FREE_LOB=1,显式释放更保险)
if (clob != null) {
try {
clob.free();
} catch (SQLException e) {
// 忽略释放异常,不影响主流程
}
}
}
}
/**
* 打印实时进度
*/
private void printProgress(long startTime) {
long elapsed = System.currentTimeMillis() - startTime;
double speed = totalInsert * 1000.0 / elapsed;
System.out.printf("⏳ 已处理 %,d 行 | 已提交 %d 次 | 已运行 %.1f 秒 | 速度 %.0f 行/秒%n",
totalInsert, totalCommit, elapsed / 1000.0, speed);
}
/**
* 数据校验:对比源表和目标表的行数
*/
private void verifyData(Connection conn) {
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("🔍 开始数据校验...");
String countSrc = "SELECT COUNT(*) FROM t_da_attach_log";
String countDst = "SELECT COUNT(*) FROM t_da_attach_log_new";
try (Statement stmt = conn.createStatement();
ResultSet rs1 = stmt.executeQuery(countSrc)) {
long srcCount = 0, dstCount = 0;
if (rs1.next()) srcCount = rs1.getLong(1);
try (ResultSet rs2 = stmt.executeQuery(countDst)) {
if (rs2.next()) dstCount = rs2.getLong(1);
}
System.out.printf("📋 源表行数: %,d%n", srcCount);
System.out.printf("📋 目标表行数: %,d%n", dstCount);
if (srcCount == dstCount) {
System.out.println("✅ 校验通过:源表和目标表行数一致");
} else {
System.out.printf("⚠️ 校验警告:行数不一致,差额 %,d 行%n", srcCount - dstCount);
}
} catch (SQLException e) {
System.err.println("⚠️ 校验过程出错: " + e.getMessage());
}
}
}
四、编译与运行步骤
4.1 准备环境
将 gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar(或对应版本的 JDBC 驱动)放到代码同一目录。
4.2 编译
bash
cd /root/clob_to_varchar
# Linux/macOS 用冒号 ":" 分隔 classpath
javac -cp ".:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" DbConfig.java TestDataGenerator.java ClobToVarcharMigration.java
# Windows 用分号 ";" 分隔
# javac -cp ".;gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" DbConfig.java TestDataGenerator.java ClobToVarcharMigration.java
4.3 生成测试数据
bash
java -cp ".:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" TestDataGenerator
预期输出:
plain
⏳ 已插入 1000 / 10000 条,耗时 x.x 秒
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 测试数据生成完成!共插入 10,000 条,总耗时 xx.xx 秒
4.4 执行迁移
bash
java -cp ".:gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar" ClobToVarcharMigration
预期输出:
plain
✅ JDBC 驱动加载成功: com.gbasedbt.jdbc.Driver
🔌 数据库连接建立成功
📋 配置参数: FETCH_SIZE=5000, BATCH_SIZE=1000, COMMIT_EVERY=5000
⏳ 开始迁移数据,请耐心等待...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⏳ 已处理 5,000 行 | 已提交 1 次 | 已运行 x.x 秒 | 速度 xxxx 行/秒
⏳ 已处理 10,000 行 | 已提交 2 次 | 已运行 x.x 秒 | 速度 xxxx 行/秒
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 数据迁移完成!
📊 总读取行数: 10,000
📊 总插入行数: 10,000
📊 总提交次数: 2
📊 CLOB 截断行数: xxx
⏱️ 总耗时: x.xx 秒
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 开始数据校验...
📋 源表行数: 10,000
📋 目标表行数: 10,000
✅ 校验通过:源表和目标表行数一致
五、800万数据量生产环境调参
表格
| 参数 | 本地测试 | 生产环境(800万行) | 说明 |
|---|---|---|---|
FETCH_SIZE |
5000 | 5000 ~ 10000 | 游标批次,减少网络往返 |
BATCH_SIZE |
1000 | 1000 ~ 2000 | 批量插入批次 |
COMMIT_EVERY |
5000 | 5000 ~ 10000 | 控制逻辑日志,避免单事务过大 |
| JVM 堆内存 | 512M | 2G ~ 4G | 留足批量缓存空间 |
| 运行时段 | 任意 | 业务低峰期 | 迁移期间目标表大量写入,避开高峰 |
生产运行前务必做两件事:
预估 CLOB 截断影响:先跑统计 SQL,确认超长记录数:
sql
dbaccess testdb - create function 'gbasedbt'.dbms_lob_getlength (clob) returns integer external name '$GBASEDBTDIR/extend/excompat.1.0/excompat.bld(dbms_lob_getlength)' language c; SELECT COUNT(*) FROM t_da_attach_log WHERE dbms_lob_getlength (op_content) > 32739;业务低峰期执行:迁移期间目标表会有大量插入,建议避开业务高峰。
六、常见问题排查
表格
| 错误信息 | 原因 | 解决 |
|---|---|---|
找不到或无法加载主类 |
java -cp 缺少当前目录 . |
java -cp ".:jar文件" 类名 |
找不到符号 DbConfig |
编译时缺少依赖 | javac -cp ".:jar文件" *.java |
不能解析例行程序 (dbms_lob_substr) |
使用了 Oracle 函数 | 已修复:改用 Java 层 Clob.getSubString() |
No suitable driver found |
JDBC URL 错误或驱动未加载 | 检查 URL 格式和 Class.forName() |
OutOfMemoryError |
FETCH_SIZE 或 BATCH_SIZE 过大 | 适当调小参数,或增大 JVM 堆内存 |
七、代码交付清单
plain
/root/clob_to_varchar/
├── gbasedbtjdbc_3.5.1_3X3_6XFDA_b9a1d9.jar # JDBC 驱动(客户提供)
├── DbConfig.java # 数据库配置
├── TestDataGenerator.java # 测试数据生成器
├── ClobToVarcharMigration.java # 核心迁移程序
└── README.md # 本文档
使用流程:
-
修改
DbConfig.java中的连接参数 -
执行数据库建表 SQL
-
编译:
javac -cp ".:jar" *.java -
生成测试数据:
java -cp ".:jar" TestDataGenerator -
执行迁移:
java -cp ".:jar" ClobToVarcharMigration -
验证数据:程序自动输出源表/目标表行数对比