Oracle 的字符集,与 MySQL 非常不同

文章目录

    今天在修复一个 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*)明确讲了两点:

    1. JVM 里的客户端字符集永远是 UCS2/UTF16(因为 Java String 就是 UTF-16),驱动负责在「数据库字符集」和「客户端字符集」之间做转换。
    2. 从 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 并不支持这种粒度的控制。