60

【茶包射手日記】超詭異 Oracle Unicode 難字問題

 3 years ago
source link: https://blog.darkthread.net/blog/weird-unicode-char-behavior-in-odpnet/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
【茶包射手日記】超詭異 Oracle Unicode 難字問題-黑暗執行緒

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」也是出現難字的跡象,這回長了見識,下次再遇到就不用走冤枉路了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK