【实战踩坑】SeaTunnel 同步 Oracle 包含 CLOB 字段表,触发 ORA-01461 报错的终极解决方案(兼顾高并发与 Upsert 更新)
一、 背景与场景
在企业级大数据同步场景中,Apache SeaTunnel 作为新一代的高性能数据集成工具,以其轻量化、分布式的特性深受开发者喜爱。
近日,在一个使用 SeaTunnel 进行 Oracle -> Oracle 全量兼增量定时同步 的项目中,我们需要同步一张核心配置表 SYS_FLOW_CONFIG。这张表的特点是包含了两个存储大文本的 CLOB 字段 (TEXT_CONTENT 和 JSON_DATA),同时业务上要求在传输过程中实现 Upsert(数据存在则更新,不存在则插入) 的语义。
然而,在配置完成后上线运行,却遭遇了 Oracle 极其棘手的底层驱动异常。
二、 遭遇地狱报错:ORA-01461
在最初的 SeaTunnel 配置文件中,为了实现 Upsert,我们在 sink 端手写了 Oracle 原生的 MERGE INTO ... USING (SELECT ? FROM DUAL) 语法。任务一启动,SeaTunnel 立即崩溃,日志抛出如下异常:
text
Caused by: org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException:
ErrorCode:[COMMON-10], ErrorDescription:[Flush data operation that in sink connector failed]
...
Caused by: java.sql.BatchUpdateException: ORA-01461: can bind a LONG value only for insert into a LONG column
at oracle.jdbc.driver.OraclePreparedStatement.executeLargeBatch(OraclePreparedStatement.java:9759)
at oracle.jdbc.driver.T4CPreparedStatement.executeLargeBatch(T4CPreparedStatement.java:1447)
🔍 为什么会触发 ORA-01461 Bug?
这个报错非常具有欺骗性,字面意思是"不能把 LONG 值绑定到非 LONG 字段"。但实际上,我们的源端和目标端字段都是 CLOB ,根本没有 LONG 字段。
根本原因在于 Oracle 旧版 JDBC 驱动在解析 PreparedStatement 批量提交时的缺陷:
- 当下游传入的 Java
String字符串长度刚好在 2000~4000 字节之间时,JDBC 驱动会陷入混淆。 - 由于我们在
MERGE语句中使用了SELECT ? FROM DUAL,DUAL虚拟表没有真实的物理类型,驱动无法感知问号?对应的究竟是个什么对象。 - 驱动自作聪明地在背后把这个
String打包成了老旧的LONG类型进行网络传输。当 Oracle 服务端收到这个包并试图塞进CLOB字段时,底层语法校验直接爆炸,抛出ORA-01461。
三、 探索与避坑尝试(反面教材)
在找到最优解之前,我们尝试了以下几种常规思路,均以失败告终:
- 尝试一:降低
batch_size = 1- 结果 :失败 。即使设为 1,SeaTunnel JDBC Sink 底层封装的
BufferedBatchStatementExecutor依然会调用驱动的executeBatch()批量接口,旧驱动顽疾依旧。
- 结果 :失败 。即使设为 1,SeaTunnel JDBC Sink 底层封装的
- 尝试二:在 SQL 中使用
CAST(? AS CLOB)或TO_CLOB(?)- 结果 :失败 。因为
FROM DUAL的虚拟层依然存在,驱动在将变量传入函数或转换函数之前,就已经在打包阶段将其判定为了LONG,完全封锁了 SQL 层面的干预。
- 结果 :失败 。因为
- 尝试三:去掉手写
query,改用 SeaTunnel 内置table+primary_keys托管模式- 结果 :失败(报 500) 。部分 Web 平台或特定引擎版本在遇到没有手写
query的 JDBC 插件时,会触发严格的大小写校验或多表元数据丢失,直接抛出:Unable to create a sink for identifier 'jdbc'。
- 结果 :失败(报 500) 。部分 Web 平台或特定引擎版本在遇到没有手写
四、 终极必杀技:PL/SQL 匿名块(100% 成功)
既然手写 MERGE + DUAL 会因为类型断层引发误判,而去掉 query 又会引发工厂初始化失败。那么唯一的解法就是:保留 query 参数顺利通过初始化,但在 SQL 内部彻底消灭 FROM DUAL!
我们利用 Oracle 的 PL/SQL 匿名块(BEGIN ... END;) 来重写 sink.query。直接在代码块内执行 先 UPDATE,若影响行数为 0 则 INSERT 的逻辑。此时问号 ? 直接以变量赋值形式与物理表的字段类型产生强绑定,驱动再也不会猜错!
💡 完美的 SeaTunnel V2 配置文件:
hocon
env {
job.mode = "BATCH"
parallelism = 1
checkpoint.interval = 86400000000
checkpoint.timeout = 86400000000
}
source {
# 源端:读取 Oracle
Jdbc {
driver = "oracle.jdbc.OracleDriver"
url = "jdbc:oracle:thin:@//<SOURCE_IP>:<PORT>/<DB_NAME>"
user = "<SOURCE_USER>"
password = "<SOURCE_PASSWORD>"
# 🔒 核心锁定:这 9 个字段的顺序,必须与下游 PL/SQL 中变量声明的问号顺序完全一致
query = """
SELECT
"ID", "BIZ_TITLE", "TEXT_CONTENT", "JSON_DATA", "CREATE_BY",
"CREATE_TIME", "UPDATE_BY", "UPDATE_TIME", "DEL_FLAG"
FROM "<SOURCE_SCHEMA>"."SYS_FLOW_CONFIG"
WHERE "DEL_FLAG" = '0'
"""
}
}
transform {
# 纯传输场景,无需转换
}
sink {
# 目标端:采用 PL/SQL 匿名块,先更新后插入。完美兼容全量 Upsert 同时彻底根治 ORA-01461
Jdbc {
driver = "oracle.jdbc.OracleDriver"
url = "jdbc:oracle:thin:@//<TARGET_IP>:<PORT>/<DB_NAME>"
user = "<TARGET_USER>"
password = "<TARGET_PASSWORD>"
# 🚀 终极改造:使用 PL/SQL 块代替 MERGE
# 声明 9 个强类型变量接收传入数据,完全局绝驱动对大对象字段(CLOB)的类型盲猜!
query = """
DECLARE
v_id "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."ID"%TYPE := ?;
v_title "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."BIZ_TITLE"%TYPE := ?;
v_desc "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."TEXT_CONTENT"%TYPE := ?;
v_data "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."JSON_DATA"%TYPE := ?;
v_c_user "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."CREATE_BY"%TYPE := ?;
v_c_time "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."CREATE_TIME"%TYPE := ?;
v_u_user "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."UPDATE_BY"%TYPE := ?;
v_u_time "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."UPDATE_TIME"%TYPE := ?;
v_del "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG"."DEL_FLAG"%TYPE := ?;
BEGIN
-- 1. 先尝试进行物理更新
UPDATE "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG" SET
"BIZ_TITLE" = v_title,
"TEXT_CONTENT" = v_desc,
"JSON_DATA" = v_data,
"CREATE_BY" = v_c_user,
"CREATE_TIME" = v_c_time,
"UPDATE_BY" = v_u_user,
"UPDATE_TIME" = v_u_time,
"DEL_FLAG" = v_del
WHERE "ID" = v_id;
-- 2. 如果更新影响的行数为 0,说明是新数据,则执行插入
IF SQL%ROWCOUNT = 0 THEN
INSERT INTO "<TARGET_SCHEMA>"."SYS_FLOW_CONFIG" (
"ID", "BIZ_TITLE", "TEXT_CONTENT", "JSON_DATA", "CREATE_BY",
"CREATE_TIME", "UPDATE_BY", "UPDATE_TIME", "DEL_FLAG"
) VALUES (
v_id, v_title, v_desc, v_data, v_c_user,
v_c_time, v_u_user, v_u_time, v_del
);
END IF;
END;
"""
# 采用 PL/SQL 块时,设置 batch_size = 1 确保块的原子提交
batch_size = 1
connection_pool_max_size = 5
is_exactly_once = false
type_mapping {
"Decimal" = "NUMBER"
"Long" = "NUMBER"
"Integer" = "NUMBER"
"String" = "VARCHAR2"
}
}
}
五、 经验总结
- 不要在旧驱动环境下对 CLOB 字段用
MERGE + DUAL:只要遇到长度在 2000~4000 字节之间局部的中等大文本,几乎必报ORA-01461。 - PL/SQL 匿名块是万能解法 :它兼顾了对其下游配置时的自定义
query初始化,又能在代码块内部自由编写复杂的IF SQL%ROWCOUNT = 0 THEN INSERT逻辑,是老旧基础设施环境大数据集成的核心秘密武器。 - 根本治理方案 :如果条件允许,从根本上解决此 Bug 的方法是更新 SeaTunnel 所在服务器的 Oracle JDBC 驱动,将
ojdbc6.jar/ojdbc7.jar升级为全新的ojdbc8-19.3.0.0或更高版本。
希望这篇避坑文章能帮到正在使用 SeaTunnel 进行大数据同步的你!欢迎点赞、收藏和转发!