Hacking樂無窮:修正Dapper+ODP.NET無法寫入Unicode問題
0 |
歷經一段時間摸索歷練,確立「新増修改用EF/ORM,查詢一律用Dapper」的最高指導原則,Dapper的簡潔、效能與彈性無可挑剔,一切看似完美,直到我膝蓋中了一箭…
無意間發現,使用Dapper+ODP.NET無法寫入Unicode字元
跟Oracle Unicode問題奮戰超過10年,以為妖孽已被降伏,用OracleDbType.NVarChar2應該就萬無一失,甚至要在CommandText中用N'…'也不是問題,萬萬沒想到Oracle Unicode問題今天又跑出來咬我屁股。
用以下範例重現問題:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Oracle.ManagedDataAccess.Client;
namespace OraUnicodeTest
{
public class Program
{
static string csOra = "…略";
static string csSql = "…略";
static void Main(string[] args)
{
using (var cn = new OracleConnection(csOra))
{
cn.Execute("UPDATE JEFFTEST SET NAME=:NAME WHERE SEQNO=:SEQNO",
new
{
SEQNO = 1,
NAME = "牛牪犇" + DateTime.Now.Millisecond
});
}
using (var cn = new SqlConnection(csSql))
{
cn.Execute("UPDATE JEFFTEST SET NAME=@NAME WHERE SEQNO=@SEQNO",
new
{
SEQNO = 1,
NAME = "牛牪犇" + DateTime.Now.Millisecond
});
}
}
}
}
用標準cn.Execute("UPDATE … SET Col=:V1 WHERE …", new { V1 })寫法同時更新Oracle及SQL寫入Unicode文字,在SQL Server一切正常,在Oracle六頭牛跑了三頭!
SQL Server
Oracle
追進Dapper原始碼,Dapper為求跨資料庫,故只能依賴IDbCommand等通用介面,使用CreateParameter產生參數物件,參數型別也必須採通用的System.Data.DbType列舉。至於字串,除非以new DbString() { Value="…", IsAnsi = true }指定System.Data.DbType.AnsiString,字串一律視為System.Data.DbType.String,支援Unicode字元。
由此推論ODP.NET在處理Parameter型別DbType.String時,理應對應成絕對支援Unicode字串的OracleDbType.NVarChar2,卻誤對應成OracleDbType.VarChar2。爬文更發現,不只Dapper,一些使用DbType.String的跨平台程式庫,遇上ODP.NET時也紛紛中箭落馬,例如:NHibernate。
用以下實驗驗證ODP.NET處理DbType.String參數有問題:
cn.Open();
var cmd = cn.CreateCommand();
var p = cmd.CreateParameter();
p.ParameterName = "N";
p.DbType = System.Data.DbType.String;
//p.OracleDbType = OracleDbType.Varchar2;
//p.OracleDbType = OracleDbType.NVarchar2;
p.Value = "牛牪犇" + DateTime.Now.Millisecond;
cmd.CommandText = "UPDATE JEFFTEST SET NAME=:N WHERE SEQNO=2";
cmd.Parameters.Add(p);
cmd.ExecuteNonQuery();
實測結果:OracleParameter設定DbType = DbType.String或設定OracleDbType = OracleDbType.Varchar2i時「犇」字都無法正確寫入;必須OracleDbType = OracleDbType.NVarchar2才會正常。
依照System.Data.DbType的設計,DbType.AnsiString對應OracleDbType.Varchar2,DbType.String對應OracleDbType.NVarchar2才合理, 怎麼都覺得是ODP.NET的錯。但為什麼這個錯誤沒有引發大量災情?猜想與它需要以下條件同時成立才會發生有關:
- Oracle資料庫未採AL32UTF8編碼
新建立的資料庫多採UTF8編碼,VARCHAR2即可存入Unicode,因此DbType.String對應成OracleDbType.Varchar2也沒差。本次處理的Oracle環境為求與老系統相容,還在使用ZHT16MSWIN950編碼。 - OracleParameter參數型別未指定OracleDbType,而是指定DbType
OracleParameter同時具備OracleDbType及DbType,都可以設定參數型別。直接使用ODP.NET時,我們多半會指定OracleDbType明確選用NVarchar2或Varchar2,只有Dapper、NHibernate這類必須跨資料庫的程式庫,才會使用與資料庫種類無關的DbType。 - 寫入內容剛好有ANSI/BIG5難字
Dapper寫入NVarchar2的做法已應用在不少地方,這次碰巧寫入資料帶有BIG5難字,問題才爆出來。
這問題挺嚴重,寫Unicode變空白或亂碼誰都不能接受,但要為此放棄Dapper?研判這是ODP.NET的Bug,但感覺被困擾的人不多,Oracle不會積極處理,難道就只能束手無策嗎?
不,誰都別想惹他媽的程式老魔人,誰都別想~ (註:有人提問,補上小河馬典故)
判斷是ODP.NET的Bug,但錯誤不普及,短期被修復的可能性不大。但不修正,Dapper無法正確更新Oracle NVARCHAR2,等同廢了一條腿,怎麼辦?
該是駭客登場的時候了,換上墨鏡跟黑色長大衣,打開JustDecomplie反組譯Oracle.ManagedDataAccess.Client.dll鎖定問題根源。
在DbType屬性的set段找到邏輯,當設定OracleParameter.DbType時,背後會同步修改m_oraDbType屬性,而什麼DbType要對應到什麼OracleDbType,由一個內部靜態類別,OraDb_DbTypeTable,的陣列資料:int[] dbTypeToOracleTypeMapping決定。
再追進OraDb_DbTypeTable,靜態建構式裡以Hard-Coding方式指定哪一種DbType列舉要對應成哪一種OracleDbType。先查出各列舉對整數:
(int)OracleDbType.NVarchar2 = 119
(int)OracleDbType.Varchar2 = 126
(int)System.Data.DbType.String = 16
dbTypeToOracleDbTypeMapping[16]=126,罪證確鑿!DbType.String被對應成OracleDbType.Varchar2,是造成Unicode字元無法寫入DB的元兇!
找到根源就好辦,在駭客眼裡,類別屬性欄位哪有分什麼public、internal、private,System.Relection拿出來,想怎麼讀就怎麼讀,愛怎麼改就怎麼改。
我寫了以下修正方法覆寫dbTypeToOracleDbTypeMapping將DbType.String改指向OracleDbType.NVarchar2,修正後ODP.NET + Dapper無法寫入Unicode問題就煙消雲散了。(註:FixOdpNetDbTypStringMapping請放在Glabal.asax.cs或程序、靜態類別啟動過程,整個Process執行一次即可)
static void FixOdpNetDbTypeStringMapping()
{
Assembly asm = typeof(OracleConnection).Assembly;
Type tOraDb_DbTypeTable = asm.GetType("Oracle.ManagedDataAccess.Client.OraDb_DbTypeTable");
var fldDbTypeMapping = tOraDb_DbTypeTable.GetField("dbTypeToOracleDbTypeMapping",
BindingFlags.Static | BindingFlags.NonPublic);
int[] mappings = (int[])fldDbTypeMapping.GetValue(null);
mappings[(int)System.Data.DbType.String] = (int)OracleDbType.NVarchar2;
fldDbTypeMapping.SetValue(null, mappings);
}
解決了一個心腹大患,Hacking樂無窮~
Comments
Be the first to post a comment