Oracle 遇難字出錯不算新鮮事,現象不外乎中文字變空白變方格變問號變亂碼,老司機們一眼便知,該怎麼做心裡有數,但這回我遇到超不一樣的變種。(這樣算有吸引詭異茶包的特殊體質嗎?)

碰到一個神奇案例,資料寫入 Oracle NVARCHAR2 時結尾會多出一個 \u0000 (ASCII 0) 字元,且只有某筆資料出錯。寫入資料庫時多出 \0 結尾字元,我還是生平第一次遇到,優先懷疑資料傳遞過程被加料,由於傳輸路上涉及 WebAPI、Dapper、Managed ODP.NET,先在好幾處加上 Log,確認 WebAPI 接收的資料是正常的,鎖定問題出在 Dapper + ODP.NET 將文字寫入 Oracle 這段。另一方面,比對問題資料與正常資料差異也有新發現,問題資料包含一個罕用字 - 沗,至此案情逐漸明朗,改朝 Oracle Unicode 難字方向偵辦,但已耗掉大半天時間。只是,難字為什麼跟 \0 有關?

試著用 Managed ODP.NET 寫一小段程式寫入「沗」並不會出錯,加上 Dapper 才重現問題。至此幾乎可確定是已知的 Dapper + ODP.NET Unicode 問題,呼叫 FixOdpNetDbTypeStringMapping() 問題就能解決。(參考:Hacking 樂無窮:修正 Dapper + ODP.NET 無法寫入 Unicode 問題) 但第一時間沒能察覺跟難字有關,錯失快速破案的機會,讓我有些扼腕。

回頭調查這個神祕的難字錯誤,只會發生用 OracleDbType.Varchar2 型別傳送「沗」字給 NVarChar2 欄位,而 Oracle 資料庫未採 AL32UTF8 編碼的情境,而「沗」字造成的現象特別到讓人印象深刻 - 不是出現空白、問號、方格或亂碼,而是「重複前一個字元,加上字串結尾多一個 \0」。

我用一個範例重現這個神奇的難字現象,不想為了測試在資料庫新增資料表,我宣告了一個變數 nc NVARCHAR2,用一個 :pIn 傳入中文字串,用 :pOut 取出 (這個技巧在 ODP.NET 練習 - 執行 PL/SQL 將結果寫入暫存資料表傳回有示範過),分別傳入不同難字組合看結果。

<%@Page Language="C#"%>
<%@Import Namespace="Dapper"%>
<%@Import Namespace="Oracle.ManagedDataAccess.Client"%>
<script runat="server">

void Page_Load(object sender, EventArgs e)
{
    using (var cn = DataHelper.GetConnection()) 
    {
        cn.Open();
        var cmd = cn.CreateCommand();
        cmd.CommandText = @"
DECLARE
    nc NVARCHAR2(16);
BEGIN
    nc := :pIn;
    :pOut := nc;
END;";
        var pIn = cmd.Parameters.Add("pIn", OracleDbType.Varchar2);
        var pOut = cmd.Parameters.Add("pOut", OracleDbType.Varchar2);
        pOut.Direction = System.Data.ParameterDirection.Output;
        pOut.Size = 128;

        Action<string> test = (t) => {
            pIn.Value = t;
            cmd.ExecuteNonQuery();
            string v = pOut.Value.ToString();
            Response.Write("<li>" + t + " = " + v + "(" + BitConverter.ToString(Encoding.UTF8.GetBytes(v)) + ")</li>");
        };
        Response.Write("<ul>");
        test("A沗");
        test("Z沗");
        test("#沗A");
        test("沗");
        test("沗字");
        test("是沗字");
        test("Z犇");
        test("D堃");
        Response.Write("</ul>");
    }
}
</script>

  • A沗 = AA(41-41-00) 重複一次A,結尾出現 \0
  • Z沗 = ZZ(5A-5A-00) 重複一次Z,結尾出現 \0
  • #沗A = ##A(23-23-41-00) 重複一次#,後方接著的 A 字元後方多出 \0
  • 沗 = ?(EF-BC-9F) 若前面無字元,傳回 ?(UTF8 = EF-BC-9F),沒出現 \0
  • 沗字 = ?字(EF-BC-9F-E5-AD-97) 若前面無字元,傳回?,後方中文字正常,無 \0
  • 是沗字 = 是是字(E6-98-AF-E6-98-AF-E5-AD-97) 前後方都是中文,傳回?,後方字元正常,無 \0
  • Z犇 = Z(5A-EE-9B-95) 犇字顯示為不可見文字
  • D堃 = D(44-EE-83-86) 堃字顯示為不可見文字

夠奇特吧?誤將 Unicode 視為 BIG5 處理,結果不可預期這點我可以接受,但從沒想過「前字元重複,字串最尾端出現\0」也是出現難字的跡象,這回長了見識,下次再遇到就不用走冤枉路了。

A very strange Unicode char behavior on ODP.NET.


Comments

Be the first to post a comment

Post a comment