ODBC连接返回中文字符为“?”

3

我有一个存储了一些简体中文数据值的Oracle数据库。我创建了一个ASP.net MVC C#网页来显示这些信息。为了检索数据,我使用了OdbcConnection,但是当我运行da.Fill(t)命令时,这些值会以"?"返回。

        OdbcCommand cmd = new OdbcCommand();
        cmd.CommandText = select;

        OdbcConnection SqlConn = new OdbcConnection("Driver={Oracle in instantclient_11_2};Dbq=Database;Uid=Username;pwd=password;");
        DataTable t = new DataTable();
        cmd.Connection = SqlConn;

        SqlConn.Open();
        OdbcDataAdapter da = new OdbcDataAdapter(cmd);
        SqlConn.Close();
        da.Fill(t);
        return t;

t 中有数据,但所有应该是中文字符的地方都只是一系列的“?????”。


请确保您的页面以UTF8格式提供。 - SLaks
你的数据库字符集是什么?你的客户端nls_lang是什么? - Justin Cave
页面可以正常显示其他硬编码的中文字符,但是数据连接返回的只有问号。 - Luke Konecki
在我的Oracle数据库上,NLS_LANGUAGE = AMERICANNLS_CHARACTERSET = AL32UTF8 - Luke Konecki
@LukeKonecki,这不是被问到的问题,问题是“你的客户端nls_lang是什么?” - Wernfried Domscheit
1个回答

16

字符集问题很常见,让我试着提供一些一般性的说明。

原则上,您需要考虑四个不同的字符集设置。

1 和 2: NLS_CHARACTERSETNLS_NCHAR_CHARACTERSET

例如:AL32UTF8

它们仅在您的数据库中定义,您可以使用以下方式查询它们:

    SELECT * 
    FROM V$NLS_PARAMETERS 
    WHERE PARAMETER IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');

这些设置定义了可以在您的数据库中存储哪些字符(以哪种格式) - 没有更多,也没有更少。如果您需要在现有数据库上更改它,则需要付出一些努力(请参见字符集迁移和/或Oracle 数据库 Unicode 迁移助手)。

您可以在字符集中找到 Oracle 支持的字符集。

3: NLS_LANG

例子:AMERICAN_AMERICA.AL32UTF8

此值仅在客户端上定义。NLS_LANG 与能否在数据库中存储字符无关。它用于让 Oracle 知道您在客户端上使用的字符集。当您设置 NLS_LANG 值(例如为 AL32UTF8)时,您只是告诉 Oracle 数据库“我的客户端使用字符集 AL32UTF8” - 这并不一定意味着您的客户端确实在使用 AL32UTF8!(见下面的#4)

NLS_LANG 可以由环境变量 NLS_LANG 或 Windows 注册表中的 HKLM\SOFTWARE\Wow6432Node\ORACLE\KEY_%ORACLE_HOME_NAME%\NLS_LANG(32 位),或者 HKLM\SOFTWARE\ORACLE\KEY_%ORACLE_HOME_NAME%\NLS_LANG(64 位)定义。根据您的应用程序,可能有其他指定 NLS_LANG 的方式,但让我们坚持基础知识。如果未提供 NLS_LANG 值,则 Oracle 默认为 AMERICAN_AMERICA.US7ASCII

NLS_LANG 的格式为 NLS_LANG=语言_地区.字符集。NLS_LANG 的 {字符集} 部分在任何系统表或视图中都不会显示。NLS_LANG 定义的所有组件都是可选的,因此以下定义都是有效的:NLS_LANG=.WE8ISO8859P1NLS_LANG=_GERMANYNLS_LANG=AMERICANNLS_LANG=ITALIAN_.WE8MSWIN1252NLS_LANG=_BELGIUM.US7ASCII

如上所述,NLS_LANG 的 {字符集} 部分在数据库的任何系统表/视图或任何函数中都不可用。严格来说,这是正确的,但您可以运行此查询:

SELECT DISTINCT CLIENT_CHARSET
FROM V$SESSION_CONNECT_INFO
WHERE (SID, SERIAL#) = (SELECT SID, SERIAL# FROM v$SESSION WHERE AUDSID = USERENV('SESSIONID'));

它应该从当前的NLS_LANG设置返回字符集,但根据我的经验,该值经常为NULL或Unknown,即不可靠。

在此处查找更多非常有用的信息:NLS_LANG FAQ

请注意,某些技术不使用NLS_LANG,因此在那里的设置不会产生任何影响,例如:

4:您的终端、应用程序或.sql文件的“真实”字符集

示例:UTF-8

如果您使用终端程序(例如SQL*Plus或isql),可以使用命令chcp来查询代码页,在Unix / Linux上相当于locale charmapecho $LANG。 您可以从此处获取所有Windows代码页标识符的列表:Code Page Identifiers。 请注意,对于UTF-8(chcp 65001),存在一些问题,请参见此讨论

如果您使用 .sql 文件和像 TOAD 或 SQL-Developer 这样的编辑器,您需要检查保存选项。通常您可以选择值,如 UTF-8ANSIISO-8859-1 等。 ANSI 表示 Windows 的 ANSI 代码页,通常是 CP1252,您可以在注册表中检查它,在 HKLM\SYSTEM\ControlSet001\Control\Nls\CodePage\ACP 或此处: 国际语言支持 (NLS) API 参考

[Microsoft 已删除此参考,可从 Web 存档 [国际语言支持 (NLS) API 参考] 中获取: 11]

如何设置所有这些值?

最重要的一点是匹配 NLS_LANG 和您的终端、应用程序的“真实”字符集,或者您的 .sql 文件的编码。

一些常见的组合包括:

  • CP850 -> WE8PC850

  • CP1252 或 ANSI(在“西方” PC 的情况下) -> WE8MSWIN1252

  • ISO-8859-1 -> WE8ISO8859P1

  • ISO-8859-15 -> WE8ISO8859P15

  • UTF-8 -> AL32UTF8

或者运行此查询以获取更多信息:

SELECT VALUE AS ORACLE_CHARSET, UTL_I18N.MAP_CHARSET(VALUE) AS IANA_NAME
FROM V$NLS_VALID_VALUES
WHERE PARAMETER = 'CHARACTERSET';

有些技术可以让生活更轻松,例如ODP.NET(未托管驱动程序)或来自Oracle的ODBC驱动程序会自动继承NLS_LANG值的字符集,因此上面的条件总是成立。

需要将客户端NLS_LANG值设置为数据库NLS_CHARACTERSET值吗?

不一定!例如,如果您的数据库字符集为NLS_CHARACTERSET=AL32UTF8,而客户端字符集为NLS_LANG=.ZHS32GB18030,则它将正常工作(前提是您的客户端确实使用GB18030),尽管这些字符集完全不同。 GB18030 是一个常用于中文的字符集,像 UTF-8 一样支持所有Unicode字符。

如果您的NLS_CHARACTERSET=AL32UTF8,而NLS_LANG=.WE8ISO8859P1,它也会工作(再次,前提是您的客户端确实使用ISO-8859-P1)。但是,数据库可能存储客户端无法显示的字符,代替显示占位符(例如 ¿)。

无论如何,如果适当的话,具有匹配的NLS_LANG和NLS_CHARACTERSET值是有益的。如果它们相等,则可以确保可以在数据库中存储的任何字符也可以被显示,并且您在终端中输入或在 .sql 文件中编写的任何字符也可以存储在数据库中,而不会被替换为占位符。

补充

很多时候,您可以读到类似“NLS_LANG字符集必须与数据库字符集相同”的建议(也在SO上)。这是错误的观念和流行的谬论!

另请参阅应该将NLS_LANG设置与数据库字符集匹配吗?

NLS_LANG字符集应反映客户端操作系统字符集的设置。例如,如果数据库字符集为AL32UTF8且客户端运行在Windows操作系统上,则不应将AL32UTF8设置为NLS_LANG参数中的客户端字符集,因为没有UTF-8 WIN32客户端。相反,NLS_LANG设置应反映客户端的代码页。例如,在英语Windows客户端上,代码页是1252。适当的NLS_LANG设置是AMERICAN_AMERICA.WE8MSWIN1252

正确设置NLS_LANG可使从客户端操作系统字符集到数据库字符集的转换正确进行。当这些设置相同时,Oracle Database假定正在发送或接收的数据编码与数据库字符集相同,因此可能不会执行字符集验证或转换。如果客户端代码页和数据库字符集不同且需要进行转换,则可能导致数据损坏。

然而,陈述“没有UTF-8 WIN32客户端”当然已经过时了!

以下是证明:

C:\>set NLS_LANG=.AL32UTF8

C:\>sqlplus ...

SQL> SET SERVEROUTPUT ON
SQL> DECLARE
  2  CharSet VARCHAR2(20);
  3  BEGIN
  4     SELECT VALUE INTO Charset FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';
  5     DBMS_OUTPUT.PUT_LINE('Database NLS_CHARACTERSET is '||Charset);
  6     IF UNISTR('\20AC') = '€' THEN
  7             DBMS_OUTPUT.PUT_LINE ( '"€" is equal to U+20AC' );
  8     ELSE
  9             DBMS_OUTPUT.PUT_LINE ( '"€" is not the same as U+20AC' );
 10     END IF;
 11  END;
 12  /

Database NLS_CHARACTERSET is AL32UTF8
"€" is not the same as U+20AC

PL/SQL procedure successfully completed.

客户端和数据库字符集都是AL32UTF8, 但是字符不匹配的原因是,我的cmd.exe以及SQL*Plus使用的是Windows CP1252编码。因此,我必须相应地设置NLS_LANG:

C:\>chcp
Active code page: 1252

C:\>set NLS_LANG=.WE8MSWIN1252

C:\>sqlplus ...

SQL> SET SERVEROUTPUT ON
SQL> DECLARE
  2  CharSet VARCHAR2(20);
  3  BEGIN
  4     SELECT VALUE INTO Charset FROM nls_database_parameters WHERE parameter = 'NLS_CHARACTERSET';
  5     DBMS_OUTPUT.PUT_LINE('Database NLS_CHARACTERSET is '||Charset);
  6     IF UNISTR('\20AC') = '€' THEN
  7             DBMS_OUTPUT.PUT_LINE ( '"€" is equal to U+20AC' );
  8     ELSE
  9             DBMS_OUTPUT.PUT_LINE ( '"€" is not the same as U+20AC' );
 10     END IF;
 11  END;
 12  /

Database NLS_CHARACTERSET is AL32UTF8
"€" is equal to U+20AC

PL/SQL procedure successfully completed.

还请考虑这个例子:

CREATE TABLE ARABIC_LANGUAGE (
    LANG_CHAR VARCHAR2(20), 
    LANG_NCHAR NVARCHAR2(20));

INSERT INTO ARABIC_LANGUAGE VALUES ('العربية', 'العربية');

您需要为单个语句设置两个不同的值 NLS_LANG,这是不可能的。

另请参见如果我们有US7ASCII字符集,为什么会让我们存储非ASCII字符?Oracle中NLS_NCHAR_CHARACTERSET和NLS_CHARACTERSET的区别


感谢您的帮助。我已经成功在我的Web服务器上设置了NLS_LANG环境变量为AMERICAN_AMERICA.AL32UTF8,问题得到了解决。 - Luke Konecki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接