☰
Oracle Long、RAW、BLOB字段读写避坑指南:SQL/PLSQL/JDBC三路实操
2026/10/9 9:45:46 网站建设 项目流程

简介:本资源是一份面向Oracle数据库开发与运维人员的实战型技术文档,聚焦Long、Raw、Blob三类关键大对象字段的读写操作实践,解决实际项目中二进制数据与超长文本存储难、操作易出错等典型问题。文档以Oracle 9i环境为基准,完整呈现C_EMP1_T表建表语句、C#代码级INSERT/UPDATE/BLOB文件上传流程(含aspx前端控件配置与服务器端File.ReadAllBytes处理)、参数绑定细节及类型适配要点,覆盖从IC卡MAC号(RAW)、用户简历(LONG)到图像文件(BLOB)的全场景示例。资源为单文件PDF,共1个514KB技术文档,内容精炼、代码可直接参考。目前已有247人学习下载,适合中初级DBA及.NET+Oracle混合开发工程师快速掌握大对象字段的安全读写规范与避坑要点。

1. Oracle里Long、RAW、BLOB这三类“难搞字段”到底在卡谁?——不是语法不会,是读写逻辑根本不同

你在Oracle里建表时随手写了LONG,结果Java程序一查就报ORA-00932;用PL/SQL往RAW字段插十六进制数据,存进去再SELECT出来却变成乱码;更别提BLOB——明明文件上传成功,前端下载却是空的或损坏的。这不是你SQL写错了,而是这三类字段压根不走普通VARCHAR2那套读写路径:LONG被Oracle官方标记为“过时但未移除”,RAW绕过字符集转换直接存二进制字节流,BLOB则必须通过LOB Locator机制操作,不能像普通字段一样INSERT INTO t VALUES (xxx)直塞。它们专治“以为数据库字段都一样”的新手,也常让有MySQL/PostgreSQL经验的开发者在Oracle项目里集体翻车。本文不讲概念定义,只聚焦一线真实场景:用最简SQL+PL/SQL+JDBC三路实操,把LONG的兼容读法、RAW的十六进制安全写法、BLOB的分块流式读写全部跑通,每一步附参数含义和失败日志特征。适合正在维护老系统、对接遗留接口、或刚接手含LOB字段Oracle库的后端/DBA。


2. Long字段:为什么它还在?又为什么必须用特殊方式读?

LONG类型是Oracle 7时代遗留的“大文本”方案,虽自Oracle 8i起就被官方建议用CLOB替代,但大量老系统(尤其金融、政务类)仍存在。它的核心限制在于:单表只能有一个LONG字段,且不能出现在WHERE/ORDER BY/GROUP BY子句中,更不能参与索引、约束、分区。但真正让开发者崩溃的是读取行为——当你执行SELECT long_col FROM t WHERE id=1,Oracle客户端(如SQL*Plus、SQL Developer)默认会截断显示(通常只显示前80字符),而JDBC驱动若未显式设置setLongDataBuffer,会直接抛出SQLException: ORA-01403: no data found(即使数据存在)。这不是数据丢了,是驱动层主动放弃读取。

2.1 用SQL*Plus安全读取Long字段的最小配置

# 启动SQL*Plus时必须加 -S 参数禁用提示,并设置行宽与长字段缓冲区 sqlplus -S username/password@//host:port/service_name << 'EOF' SET LINESIZE 32767 SET LONG 1000000 SET PAGESIZE 0 SELECT long_col FROM your_table WHERE id = 1; EXIT; EOF

逻辑说明:SET LONG 1000000告诉SQL*Plus最多读取100万字符,否则默认只读80;SET LINESIZE 32767防止长文本自动换行;-S避免输出连接信息干扰解析。若不设LONG值,SELECT返回的只是<LONG>占位符。

2.2 PL/SQL中读取Long字段:必须用DBMS_SQL包绕过限制

DECLARE l_cursor INTEGER; l_long_data VARCHAR2(32767); l_buffer VARCHAR2(32767); l_amount BINARY_INTEGER := 32767; l_offset INTEGER := 1; BEGIN l_cursor := DBMS_SQL.OPEN_CURSOR; DBMS_SQL.PARSE(l_cursor, 'SELECT long_col FROM your_table WHERE id = :id', DBMS_SQL.NATIVE); DBMS_SQL.BIND_VARIABLE(l_cursor, ':id', 1); IF DBMS_SQL.EXECUTE_AND_FETCH(l_cursor) > 0 THEN -- 关键:用DBMS_SQL.COLUMN_VALUE_LONG读取LONG,不能用COLUMN_VALUE DBMS_SQL.COLUMN_VALUE_LONG(l_cursor, 1, l_buffer, l_amount, l_offset, l_long_data); DBMS_OUTPUT.PUT_LINE('Long content length: ' || LENGTH(l_long_data)); DBMS_OUTPUT.PUT_LINE(SUBSTR(l_long_data, 1, 200)); -- 打印前200字符 END IF; DBMS_SQL.CLOSE_CURSOR(l_cursor); END; /

参数说明:COLUMN_VALUE_LONG是唯一能安全读取LONG的API;l_amount设为32767是最大单次读取长度(Oracle限制);l_offset从1开始,若内容超长需循环调用并累加offset。注意:此方法仅适用于PL/SQL环境,JDBC需另走路径。

2.3 JDBC读取Long字段:必须关闭自动提交并显式获取流

// Java代码片段(使用Oracle JDBC 19c driver) String sql = "SELECT long_col FROM your_table WHERE id = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 必须关闭自动提交!否则LONG读取失败 ps.setLong(1, 1L); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 关键:用getAsciiStream()而非getString() InputStream is = rs.getAsciiStream("long_col"); if (is != null) { String longContent = new String(is.readAllBytes(), StandardCharsets.UTF_8); System.out.println("Long content length: " + longContent.length()); } } } }

踩坑点:getString()对LONG字段会返回null或截断;getAsciiStream()才是正确入口;setAutoCommit(false)是Oracle JDBC驱动硬性要求,否则getAsciiStream()返回null。驱动版本低于12.1可能需用getBinaryStream(),但语义上LONG应视为ASCII文本。


3. RAW字段:十六进制存储的“零损耗”通道,但写错一个字节就全废

RAW是Oracle中唯一原生支持二进制字节存储的标量类型(非LOB),常用于存加密密钥、哈希值、硬件设备ID等。它不经过字符集转换,存什么字节就返什么字节——这是优势,也是雷区。比如你用HEXTORAW('FF')插入,SELECT出来确实是FF;但若误写成HEXTORAW('ff')(小写),某些旧版驱动会报错;更常见的是Java端用getBytes()直接塞字符串,结果因平台默认编码(如GBK)导致字节错乱。RAW字段最大长度2000字节,超过必须用BLOB。

3.1 SQL中安全写入RAW字段:HEXTORAW函数是唯一可信入口

-- 正确:全大写十六进制字符串,长度为偶数 INSERT INTO your_table (id, raw_col) VALUES (1, HEXTORAW('A1B2C3D4E5F6')); -- 错误示例(运行时可能报ORA-01465) -- INSERT INTO your_table (id, raw_col) VALUES (1, HEXTORAW('a1b2')); -- 小写在部分版本不支持 -- INSERT INTO your_table (id, raw_col) VALUES (1, HEXTORAW('A1B2C')); -- 奇数长度,ORA-01465: invalid hex number -- 查询时用RAWTOHEX确保可读性 SELECT id, RAWTOHEX(raw_col) AS raw_hex FROM your_table WHERE id = 1;

逻辑说明:HEXTORAW()严格校验输入:必须是偶数长度、仅含0-9A-F字符;RAWTOHEX()是反向转换,用于调试。永远不要用UTL_RAW.CAST_TO_RAW('string')存业务数据——它按数据库字符集编码,不是纯字节。

3.2 PL/SQL中构造RAW:用UTL_RAW包拼接,避免隐式转换

DECLARE l_raw_val RAW(2000); BEGIN -- 安全拼接:用UTL_RAW.CONCAT拼接已知HEXTORAW结果 l_raw_val := UTL_RAW.CONCAT( HEXTORAW('A1B2'), HEXTORAW('C3D4'), HEXTORAW('E5F6') ); INSERT INTO your_table (id, raw_col) VALUES (2, l_raw_val); COMMIT; -- 验证:SELECT RAWTOHEX(raw_col) 应返回 'A1B2C3D4E5F6' END; /

参数说明:UTL_RAW.CONCAT接受多个RAW参数,无字符集风险;UTL_RAW.CAST_TO_RAW('ABC')会将字符串按数据库字符集(如AL32UTF8)转字节,若字符串含中文则结果不可控,禁止用于业务数据。

3.3 JDBC写入RAW字段:必须用setBytes(),且字节数组需预处理

// Java代码:生成确定字节序列,不依赖字符串编码 byte[] keyBytes = new byte[]{(byte)0xA1, (byte)0xB2, (byte)0xC3, (byte)0xD4}; // 或从十六进制字符串解析(推荐,避免手写0x) String hexStr = "A1B2C3D4"; byte[] parsedBytes = parseHexStrToByte(hexStr); // 自定义工具方法 try (PreparedStatement ps = conn.prepareStatement("INSERT INTO your_table (id, raw_col) VALUES (?, ?)")) { ps.setLong(1, 2L); ps.setBytes(2, parsedBytes); // 关键:必须用setBytes(),不能setString() ps.executeUpdate(); } // parseHexStrToByte实现(安全解析大写十六进制) public static byte[] parseHexStrToByte(String hexStr) { if (hexStr.length() % 2 != 0) throw new IllegalArgumentException("Hex string length must be even"); byte[] result = new byte[hexStr.length() / 2]; for (int i = 0; i < hexStr.length(); i += 2) { result[i / 2] = (byte) ((Character.digit(hexStr.charAt(i), 16) << 4) + Character.digit(hexStr.charAt(i+1), 16)); } return result; }

踩坑点:setString()会触发字符集转换,setBytes()直传字节;parseHexStrToByte必须处理大写A-F(OracleHEXTORAW只认大写);若用Apache Commons Codec的Hex.decodeHex(),需确保传入char数组为大写。


4. BLOB字段:不是“大字符串”,是必须用流操作的独立对象

BLOB(Binary Large Object)是Oracle处理超大二进制数据的标准方案,上限4GB。它和LONG本质不同:BLOB是独立的LOB段(segment),表中只存一个指向它的locator(定位器),所有读写必须通过DBMS_LOB包或JDBC的Blob接口进行。直接INSERT INTO t VALUES (EMPTY_BLOB())只是创建空locator,必须用DBMS_LOB.WRITE()或setBinaryStream()填充内容。这也是为什么前端上传文件后查BLOB字段长度为0——locator建了,但没人往里面写数据。

4.1 PL/SQL中写入BLOB:三步法(初始化+写入+提交)

-- 第一步:插入空BLOB locator INSERT INTO your_table (id, blob_col) VALUES (1, EMPTY_BLOB()) RETURNING blob_col INTO :blob_loc; -- 第二步:在PL/SQL块中用DBMS_LOB操作locator DECLARE l_blob BLOB; l_bfile BFILE; BEGIN SELECT blob_col INTO l_blob FROM your_table WHERE id = 1 FOR UPDATE; -- 方式1:从操作系统文件加载(需DIRECTORY对象) -- l_bfile := BFILENAME('MY_DIR', 'image.jpg'); -- DBMS_LOB.FILEOPEN(l_bfile, DBMS_LOB.FILE_READONLY); -- DBMS_LOB.LOADFROMFILE(l_blob, l_bfile, DBMS_LOB.GETLENGTH(l_bfile)); -- DBMS_LOB.FILECLOSE(l_bfile); -- 方式2:从字节数组写入(更常用) DBMS_LOB.WRITEAPPEND(l_blob, 4, UTL_RAW.CAST_TO_RAW('ABCD')); DBMS_LOB.WRITEAPPEND(l_blob, 4, UTL_RAW.CAST_TO_RAW('EFGH')); COMMIT; END; /

逻辑说明:EMPTY_BLOB()生成空locator;FOR UPDATE锁定行,防止并发写冲突;DBMS_LOB.WRITEAPPEND追加写入,比WRITE更安全(无需管理offset);UTL_RAW.CAST_TO_RAW()在此处安全,因输入是ASCII字符串。切记:没有COMMIT,写入不生效。

4.2 JDBC写入BLOB:用setBinaryStream()分块,避免内存溢出

// Java代码:流式写入,不加载整个文件到内存 FileInputStream fis = new FileInputStream("/path/to/large_file.zip"); try (PreparedStatement ps = conn.prepareStatement("INSERT INTO your_table (id, blob_col) VALUES (?, ?)")) { ps.setLong(1, 3L); // 关键:getBinaryStream()返回OutputStream,write()分块写入 try (OutputStream os = ((oracle.sql.BLOB) ps.getParameterMetaData().getParameterType(2) == oracle.jdbc.OracleTypes.BLOB ? ((oracle.sql.BLOB) ps.getObject(2)).getBinaryOutputStream() : null).orElseThrow()) { // 更可靠写法:用setBinaryStream()直接获取OutputStream OutputStream os2 = ps.setBinaryStream(2); // Oracle JDBC 12c+ 支持 byte[] buffer = new byte[8192]; int len; while ((len = fis.read(buffer)) != -1) { os2.write(buffer, 0, len); } os2.close(); } ps.executeUpdate(); }

参数说明:setBinaryStream(2)返回OutputStream,直接写入BLOB;buffer大小建议8KB(8192),过大易OOM,过小IO频繁;ps.executeUpdate()前必须关闭流,否则数据不提交。不要用setBytes(byte[])——它会把整个文件加载进JVM内存,100MB文件直接OOM。

4.3 JDBC读取BLOB:用getBinaryStream()流式下载,禁用getBytes()

// Java代码:安全读取BLOB到文件 String sql = "SELECT blob_col FROM your_table WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setLong(1, 3L); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { InputStream is = rs.getBinaryStream("blob_col"); // 关键:不是getBlob().getBytes() if (is != null) { Files.copy(is, Paths.get("/tmp/downloaded_file.zip"), StandardCopyOption.REPLACE_EXISTING); System.out.println("BLOB saved successfully"); } } } }

踩坑点:getBlob().getBytes()会把整个BLOB加载进内存,1GB文件直接崩溃;getBinaryStream()返回InputStream,可流式处理;Files.copy()是JDK7+推荐方式,自动处理缓冲。


5. 避坑指南:Long/Raw/Blob三大字段的5个血泪现场

这些坑不是文档里写的“注意事项”,而是某开发者在凌晨三点重启应用时发现的真问题。每一条都带现象、原因、解决,照着查日志就能定位。

5.1 现象:JDBC查询含LONG字段的表,ResultSet.next()返回false,但表里明明有数据

原因:JDBC连接未关闭自动提交(conn.setAutoCommit(false)),或驱动版本低于12.1未正确处理LONG locator。
解决:强制设置conn.setAutoCommit(false);升级Oracle JDBC驱动至19c以上;改用getAsciiStream()读取。

5.2 现象:PL/SQL中SELECT raw_col INTO l_raw FROM t后,DBMS_OUTPUT.PUT_LINE(RAWTOHEX(l_raw))输出NULL

原因:raw_col字段值为NULL,但RAWTOHEX(NULL)返回NULL,而非空字符串,容易误判为查询失败。
解决:先检查l_raw IS NULL,再调用RAWTOHEX;或用NVL(RAWTOHEX(raw_col), 'NULL')在SQL层处理。

5.3 现象:Java用setBytes()写入RAW字段,SELECT出来十六进制值与预期不符(如A1B2变成C2A1C2B2)

原因:Java字节数组构造错误,如用"A1".getBytes()得到UTF-8编码字节(C2 A1),而非十六进制解析的A1。
解决:必须用parseHexStrToByte("A1B2")等工具方法解析十六进制字符串,禁用String.getBytes()。

5.4 现象:BLOB写入后DBMS_LOB.GETLENGTH()返回0,但SELECT blob_col FROM t在SQL*Plus中显示(BLOB)

原因:未对BLOB locator执行DBMS_LOB.WRITE或WRITEAPPEND,只插入了EMPTY_BLOB(),locator为空。
解决:确认PL/SQL块中有DBMS_LOB.WRITEAPPEND调用;JDBC中确认setBinaryStream()后调用了executeUpdate()。

5.5 现象:前端下载BLOB文件,打开提示“文件已损坏”,但用DBMS_LOB.SUBSTR()查前100字节显示正常

原因:JDBC读取时用了getBlob().getBytes(),导致大文件被截断(JDBC驱动有内部缓冲限制);或HTTP响应头Content-Length未正确设置。
解决:强制用getBinaryStream()流式读取;后端计算BLOB长度SELECT DBMS_LOB.GETLENGTH(blob_col) FROM t并设Content-Length响应头。


6. 进阶技巧:用DBMS_LOB包做BLOB内容校验与分块迁移

当你要验证BLOB内容是否完整,或把老系统LONG字段迁移到BLOB时,DBMS_LOB包的底层能力就派上用场了。这里不讲理论,只给两个能直接抄的脚本:一个是校验BLOB的MD5(避免传输损坏),一个是LONG到BLOB的原子迁移(不锁表、不丢数据)。

6.1 校验BLOB完整性:用DBMS_CRYPTO生成MD5,比应用层更可靠

-- 创建函数:返回BLOB的MD5哈希值(16进制字符串) CREATE OR REPLACE FUNCTION blob_md5(p_blob IN BLOB) RETURN VARCHAR2 IS l_hash RAW(16); BEGIN IF p_blob IS NULL THEN RETURN NULL; END IF; l_hash := DBMS_CRYPTO.HASH(p_blob, DBMS_CRYPTO.HASH_MD5); RETURN RAWTOHEX(l_hash); END; / -- 使用:SELECT id, blob_md5(blob_col) AS md5_hash FROM your_table WHERE id = 1; -- 对比应用层计算的MD5,若一致则BLOB未损坏

为什么比Java校验强:DBMS_CRYPTO.HASH在数据库服务端计算,避免网络传输中的字节丢失;RAWTOHEX输出标准大写十六进制,与JavaMessageDigest结果完全一致。注意:DBMS_CRYPTO需EXECUTE权限,生产环境需DBA授权。

6.2 Long到Blob原子迁移:用DBMS_LOB.CREATETEMPORARY避免锁表

-- 场景:将表t_old的long_col迁移到t_new的blob_col,要求不停服 DECLARE l_blob BLOB; l_long LONG; BEGIN -- 1. 创建临时BLOB(不占用表空间,会话级) DBMS_LOB.CREATETEMPORARY(l_blob, TRUE); -- 2. 逐行读LONG,写入临时BLOB FOR r IN (SELECT id, long_col FROM t_old WHERE ROWNUM <= 1000) LOOP -- 将LONG转为RAW再写入BLOB(规避LONG限制) DBMS_LOB.WRITEAPPEND(l_blob, LENGTH(r.long_col), UTL_RAW.CAST_TO_RAW(r.long_col)); -- 3. 插入新表(BLOB列存临时BLOB) INSERT INTO t_new (id, blob_col) VALUES (r.id, l_blob); -- 4. 重置临时BLOB供下一行使用 DBMS_LOB.TRIM(l_blob, 0); END LOOP; -- 5. 提交(临时BLOB自动释放) COMMIT; END; /

关键设计:DBMS_LOB.CREATETEMPORARY创建会话级临时LOB,不锁源表;DBMS_LOB.TRIM(l_blob, 0)清空内容复用,避免反复创建;ROWNUM <= 1000分批处理,防内存溢出。迁移后用blob_md5()校验一致性。

6.3 一个我坚持十年的习惯:所有LOB操作必加超时与重试

在生产环境,DBMS_LOB操作可能因LOB段争用而卡住(尤其高并发写BLOB)。我的做法是在PL/SQL中封装带超时的写入:

-- 封装函数:带超时的BLOB写入 CREATE OR REPLACE FUNCTION safe_blob_write( p_blob IN OUT BLOB, p_buffer IN RAW, p_timeout_sec IN NUMBER DEFAULT 30 ) RETURN BOOLEAN IS l_start_time NUMBER := DBMS_UTILITY.GET_TIME; BEGIN WHILE DBMS_UTILITY.GET_TIME - l_start_time < p_timeout_sec * 100 LOOP BEGIN DBMS_LOB.WRITEAPPEND(p_blob, UTL_RAW.LENGTH(p_buffer), p_buffer); RETURN TRUE; EXCEPTION WHEN OTHERS THEN IF SQLCODE = -30036 THEN -- ORA-30036: unable to extend segment DBMS_LOCK.SLEEP(0.1); -- 等待100ms重试 CONTINUE; ELSE RAISE; END IF; END; END LOOP; RETURN FALSE; -- 超时 END; /

为什么有效:ORA-30036是LOB段空间争用典型错误,DBMS_LOCK.SLEEP()让出CPU,避免死循环;DBMS_UTILITY.GET_TIME精度为百分之一秒,p_timeout_sec * 100实现秒级超时。这个习惯让我在某次千万级BLOB导入中,避免了3次服务中断。

希望帮到你。

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

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

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

立即咨询