时间:2021-07-01 10:21:17 帮助过:12人阅读
3.2.工具很重要
在测试之前,为避免工具本身的特性给人造成的困惑,介绍一下几个客户端工具对UNICODE 的支持情况:
ü SQLPLUS:不支持UNICODE字符集。是否支持中文取决于当前的OEM code page,如果是cp437,无论输入还是显示中文都是不可能的。但如果是cp936,则可以支持中文输入输出。
ü PLSQL Developer:7.0版本的查询结果窗口支持UNICODE字符集,但是编辑窗口(即输入SQL语句的窗口)不支持。8.0版完全支持UNICODE。
ü Oracle SQL Developer:查询结果窗口与编辑窗口都支持UNICODE字符集。
3.3.出现乱码了
这里使用Oracle SQL Developer,分别在A、B中插入并查询中文:
View Code暂时先跳过VARCHAR2字段,先来关注NVARCHAR2字段,为什么在A库不能正常显示?无非有这几种可能:
ü 客户端操作系统不支持显示中文。
ü Oracle客户端工具(这里是Oracle SQL Developer)不支持显示中文。
ü Oracle客户端有相关设置(比如NLS_LANG)不正确。
ü 存储在数据库中的数据已经是不正确的数据。
第一点,客户端操作系统是否支持中文对运行于其上的应用程序有影响吗?应该有两种情况,一种是应用程序依赖于操作系统的中文支持;另一种是有一些软件自己带有语言包及字体(比如Adobe的一些产品,.NET程序在编译的时候也可以选择将字体文件打包进去),那么它应该不依赖于操作系统。
我猜测Oracle SQL Developer应该是属于前一种,同时我检查了操作系统,确定其已经支持东亚语言(Control panel—Regional and language options—Language tab—Supplemental languages support—Install files for East Asian languages,如果checkbox已经选中,说明已经安装东亚语言包)。
第二点,无论查询结果窗口还是编辑窗口都支持UNICODE字符集。
第三点,由于不依赖于Oracle client的OCI,客户端注册表中的NLS_LANG设置对像Oracle SQL Developer没有影响。
第四点,我们借助DUMP()函数来确定NVARCHAR2字段中具体的内容。
DUMP()的语法:DUMP(<value>[,<format>[,<offset>[,<length>]]])
其中的format参数:如果是8则表示结果使用8进制表示,如果是16则表示16进制,如果是0到16间的其它数则都使用10进制。如果是大于16的数,则分几种情况:如果是可见的ASCII字符则直接打印此字符,如果是控制字符则打印成“^x”,其它情况则把结果按16进制显示。为format加上1000则表示除了输出结果之外,还会附带输出所使用的字符集信息。
这里我们使用:
View Code我们知道“中”字的UTF-16编码是4E2D,显然在A库中存储的数据已经是不对的,00BF实际上就是一个倒的问号字符,其存储在数据库中的原始数据已经不对了,更何况是客户端的显示。
3.4.找不同
那么为什么两个库会不一样呢?嫌疑很快就落在了数据库字符集上,因为A和B的区别只在数据库字符集上,一个是WE8MSWIN1252,另一个是AL32UTF8。经过测试,结论是:
Oracle SQL Developer忽略NLS_LANG,字符串直接以照数据库字符集进行编码后由客户端传输到服务器端。由于A库数据库字符集不支持汉字,很不幸地被替换成了默认的BF并最终被存储到数据库中,永远地错下去。B库则相反,中文在传输的过程中“存活”下来并成功到达服务器端,最终自动转换成NVARCHAR2所用的编码并存储到库中。
3.5.如何让NVARCHAR2字段工作
看起来似乎A库中的NVARCHAR2字段永远也无法正常使用了,并非这样,对于Oracle SQL Developer,通过一些设置,就可以让NVARCHAR2可以正常地插入、查询。
找到{ORACLE_HOME}\sqldeveloper\sqldeveloper\bin\sqldeveloper.conf(依赖于你的Oracle SQL Developer安装路径),添加一行配置:
AddVMOption -Doracle.jdbc.convertNcharLiterals=true
同时在中文字符串前添加“N”前缀:
View Code这个配置起到的作用是这样的:在INSERT语句从客户端传输到服务器端之前,Oracle SQL Developer检测(实际上是JDBC检测)语句,如果发现“N”前缀,则事先将这部分的字符串按UTF-16进行编码得到16进制串。也就是相当于执行了这个命令:
INSERT INTO charset_test VALUES(2,’中’,UNISTR(‘\4e2d‘));
C#不需要做特殊的配置来让NVARCHAR2正常工作,只需要在执行INSERT时使用参数并选择正确的参数类型选:
View Code
4.客户端的NLS_LANG设置及编码转换
前面我说过Oracle SQL Developer忽略客户端NLS_LANG设置,那么对于其它的工具呢?(这里我们主要关注字符集及编码,不讨论NLS_LANG对日期格式、排序方式、数字显示格式等等的影响)
ü SQLPLUS,插入与查询都依赖于客户端NLS_LANG设置。通常,客户端NLS_LANG设置要与当前的OEM Codepage一致,比如US8PC437。
ü PL/SQL Developer,插入与查询都依赖于客户端NLS_LANG设置。通常,客户端NLS_LANG设置要与数据库字符集一致。
使用SQLPLUS可以清晰地看到Oracle编码转换的过程:
1) 在Oracle客户端向服务器端提交SQL语句时,Oracle客户端根据NLS_LANG和数据库字符集,对从应用程序接传送过来的字符串编码进行转换处理。如果NLS_LANG与数据库字符集相同,不作转换,否则要转换成数据库字符集并传送到服务器。服务器在接收到字符串编码之后,对于普通的CHAR或VARCHAR2类型,直接存储;对于NCHAR或NVARCHAR2类型,服务器端将其转换为国家字符集再存储。
2) 在查询数据时,服务器端原样返回存储在库中的数据,由客户端根据返回的元数据中的字符集信息与NLS_LANG和NLS_NCHAR的设置进行比较(如果NLS_NCHAR没有设置,则其默认值为NLS_LANG中的字符集设置),如果元数据中的字符集信息与客户端设置一致,不进行转换,否则要进行转换。国家字符集的转换根据NLS_NCHAR设置进行转换。
这里我也举几个使用SQLPLUS的测试例子,分别在A、B两库执行相同的语句,然后通过网络抓包查看从Oracle client传输到服务器的具体内容。
例1 客户端NLS_LANG:WE8MSWIN1252
SQL命令:insert into charset_test values(1,‘æ‘,null);
网络抓包(A库,数据库字符集为WE8MSWIN1252):91
解释:由于应用程序(即SQLPLUS)使用的编码是Codepage437,所以æ的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是Codepage1252,又由于NLS_LANG设置与数据库字符集一致,于是Oracle client不进行编码转换,91被直接传给服务器并存储,考虑到数据库字符集是Codepage1252,很显然91是错误的数据(字符[æ]在Codepage1252下的编码是E6,而非91)。
这个错误导致了一个有趣的现象,那就是在同一个客户端使用SQLPLUS查询居然可以看到正确字符[æ],这是由于SELECT的时候91也被直接返回,并且在Oracle client也不进行编码转换而是直接传给了应用程序,恰巧应用程序根据自己使用的编码可以正确解析91。但是换一个客户端机器,或者换一个客户端工具都可能得到不一样的查询结果。
网络抓包(B库,数据库字符集为AL32UTF8):E2 80 98
解释:由于应用程序(即SQLPLUS)使用的编码是Codepage437,所以æ的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是Codepage1252,而91在Codepage1252中对应的是字符[‘],根据Codepage1252到数据字符集UTF8的转换,最终转换成了E2 80 98,即UTF8下的[‘]。
例2客户端NLS_LANG:US7ASCII
SQL命令:insert into charset_test values(1,‘æ‘,null);
网络抓包(A库):BF
解释:由于应用程序(即SQLPLUS)使用的编码是Codepage437,所以æ的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是ASCII,而91在ASCII中是无效编码,根据ASCII到数据字符集Codepage1252的转换,最终转换成了BF,BF是Codepage1252遇到无效编码时使用的默认替换编码。
网络抓包(B库): EF BF BD
解释:由于应用程序(即SQLPLUS)使用的编码是Codepage437,所以æ的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是ASCII,而91在ASCII中是无效编码,根据ASCII到数据字符集UTF8的转换,最终转换成了EF BF BD,EF BF BD是UTF8遇到无效编码时使用的默认替换编码。
例3客户端NLS_LANG:US8PC437
SQL命令:insert into charset_test values(1,‘æ‘,null);
网络抓包(A库):E6
解释:E6是字符[æ]的正确的Codepage1252编码,此次由于应用程序(即SQLPLUS)使用的是Codepage437,Oracle client从NLS_LANG获得的编码信息也是Codepage437,于是进行了正确的编码转换。
网络抓包(B库):C3 A6
解释:C3 A6是字符[æ]的正确的UTF8编码,此次由于应用程序(即SQLPLUS)使用的是Codepage437,Oracle client从NLS_LANG获得的编码信息也是Codepage437,于是进行了正确的编码转换。
我觉得,只有SQLPLUS的日子总是那么美好,一切看起来既合理又可解释。当其它工具出现之后,世界就变得一团乱麻了,Oracle SQL Developer完全忽略客户端NLS_LANG设置倒是让事情变得简单,不过PL/SQL Developer则是另一回事,我花了4天时间企图搞明白其中的编码转换过程,最终只证明它就是个不可理喻的玩意儿,唯一目前看起来还正确的结论是:如果要用PL/SQL Developer,只好还是把NLS_LANG设置得跟数据库字符集一致。其它就只能自求多福了。
5.NLS_LANG对ODP.NET的影响
唯一受客户端NLS_LANG影响的是OracleString的GetNonUnicodeBytes()方法,此方法依赖于客户端本地设置的字符集,例如我们把NLS_LANG从AMERICAN_AMERICA.WE8MSWIN1252改成AMERICAN_AMERICA.US7ASCII
其中230(即HexE6)正是字符‘æ’的编码,而63(即Hex3F)是ASCII中的问号(由于ASCII字符集中没有‘æ’,故用问号代替)。
6.关于VARCHAR2, NVARCHAR2的其它问题
NVARCHAR2(N),其中的N是指字符数,不是字节数。不过其最大长度是以字节为单位,即4000字节。
VARCHAR2(N),其中的N可能是指字符数,也可能是指字节数。你可以显式地在声明的时候指定,比如VARCHAR2(10 BYTE)或者VARCHAR2(10 CHAR),未显式指明时,则由参数NLS_LENGTH_SEMANTICS决定。需要注意的是你能成功声明VARCHAR2(4000 CHAR)并不能保证你能真的存储4000个字符,如果超过4000字节,该报错Oracle还是会报错。
【参考及引用】:
1. http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html
2. http://www.laoxiong.net/category/oracle/orainternal/page/2
3. http://en.wikipedia.org/wiki/Windows-1252
4. http://en.wikipedia.org/wiki/ASCII
5. http://en.wikipedia.org/wiki/Code_page_936
6. http://en.wikipedia.org/wiki/Code_page_437
7. http://en.wikipedia.org/wiki/UTF-8
8. http://en.wikipedia.org/wiki/UTF-16/UCS-2
9. http://en.wikipedia.org/wiki/UTF-32/UCS-4
10. http://en.wikipedia.org/wiki/GB_18030
11. http://en.wikipedia.org/wiki/Unicode
12. OReilly Oracle PL SQL Programming 5th Edition,Steven Feuerstein, Bill Pribyl
13. http://www.laruence.com/2009/08/22/1059.html
14. http://en.wikipedia.org/wiki/Windows_code_page
oracle字符集
标签:.net 个数 浮云 lap -- client 语言 完全 bae