今天在修复一个 RuoYi (Sprint Boot)的操作日志写入 bug 时,发现 Oracle 的字符集跟之前使用的 MySQL 有很多不同的地方。这里记录一下。
一个字符集引起的 bug
uncategorized SQLException; SQL state [72000]; error code [12899]; ORA-12899: 列 “XXX”.“SYS_OPER_LOG”.“OPER_PARAM” 的值太大 (实际值: 2012, 最大值: 2000)
虽然我觉得 RuoYi 有点山寨,但是也不至于有这么低级的 bug。让 AI 分析了一下,果然,并不是一个简单没有判断参数值长度的低级 bug。由于 Oracle 的 varchar2 类型是字节长度,而不是字符个数,所以写入前的截断并不能规避异常。
但这个并不是我感兴趣的部分,而是我发现正在使用的 Oracle 居然使用的是 ZHS16GBK 字符集。
判断 Oracle 的字符集类型
SELECT parameter, value FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';
NLS_CHARACTERSET ZHS16GBK
GBK 编码并没有引起乱码
我有个疑问,如果数据库使用的是 ZHS16GBK,但是我看配置文件里数据库相关配置并没有指定编码集,当前使用的框架,例如 Spring Boot 能正确处理写入和查询么?而我测试了几个月居然一次也没有发现乱码问题。
AI 给出的结论是:能正确处理,而且不需要在配置里指定字符集。 这不是“碰巧”,而是 Oracle JDBC 的设计——characterEncoding 那套是 MySQL 的参数,Oracle 根本不吃这一套。(说话还挺俏皮。。。)
Oracle JDBC 的字符集机制
关键在驱动这一层,应用层(Spring Boot / MyBatis / Druid)全程只传 java.lang.String,它们对数据库字符集是无感的。真正的编码边界只有一个:JDBC 驱动的 socket 层。
浏览器 (UTF-8) → Tomcat/Jackson (UTF-16 内存) → JDBC 驱动 → Oracle (ZHS16GBK)
Java String 全程 UTF-16 此处做 双向转换
Oracle 官方文档(*JDBC Developer’s Guide – Globalization Support*)明确讲了两点:
- JVM 里的客户端字符集永远是 UCS2/UTF16(因为 Java String 就是 UTF-16),驱动负责在「数据库字符集」和「客户端字符集」之间做转换。
- 从 Oracle 10g 起,
NLS_LANG不再是 JDBC 全球化机制的一部分;JDBC 驱动不检查 NLS 环境变量,设置它对 JDBC 没有任何效果。(NLS_LANG只对 OCI 驱动 / sqlplus / PLSQL Developer 这类客户端工具有意义。)
也就是说:驱动在建立连接时就从服务端拿到了库的字符集(ZHS16GBK),插入时把 UTF-16 编码成 GBK、查询时再解码回 UTF-16,完全透明。
数据库级别还是表级别
ORACLE 这个配置是数据库全局的么? 还是每个数据表也可以有自己独立的编码集?
NLS_CHARACTERSET 是在创建数据库时确定的,用于定义数据库存储 CHAR、VARCHAR2、CLOB 等类型数据所使用的字符集。一旦数据库创建完成,该设定便应用于整个数据库,无法在表或列级别进行更改。
这与 MySQL 等数据库不同。在 Oracle 中,字符集是存储层的全局属性,决定了数据在数据库内部的编码方式;而表或列的字符集属于逻辑层概念,Oracle 并不支持这种粒度的控制。