Dapper DynamicParameters + ODP.NET 中文亂碼問題
1 |
自從在 Hacking 樂無窮:修正 Dapper+ODP.NET 無法寫入 Unicode 問題想出 FixOdpNetDbTypeStringMapping 大絕,以為我已經收服 ODP.NET + Dapper Unicode 妖魔,並沒有,昨天又被咬一口。
踩中的是 DynamicParameters 的坑,範例程式如下,我分別用匿名型別及 DynamicParameters 傳入參數 INSERT 進資料表,執行前有記得呼叫 FixOdpNetDbTypeStringMapping() :
public void Test()
{
FixOdpNetTypeStringMapping();
using (var cn = GetConnection())
{
cn.Execute("TRUNCATE TABLE DAPPERTEST");
var cmdText = "INSERT INTO DAPPERTEST VALUES (:I, :T)";
cn.Execute(cmdText, new
{
I = 1, T = "张飞犇"
});
var d = new DynamicParameters();
d.Add("I", 2);
d.Add("T", "张飞犇");
cn.Execute(cmdText, d);
}
}
實測結果,使用匿名型別參數傳入的"张飞犇"有正常寫入,改用 DynamicParameters 傳入的"张飞犇",簡體「张」變繁體「張」,另兩個難字則變磚頭。
依據之前的經驗,簡體變繁體是字串被當成 VARCHAR2 處理造成的。問題是我有呼叫 FixOdpNetTypeStringMapping() 且匿名型別傳參數是成功的,由此推測是 DynamicParameters 對映 OracleParameter 邏輯不同造成。我還試著在 Add() 時指定 DbType.String,無效!
d.Add("T", "张飞犇", System.Data.DbType.String);
不得已,Use the source, Jeffrey! 快使用原力原始碼!
在 Github 找到 DynamicParameters 原始碼,轉換 IDbParameter 的邏輯寫在 protected void AddParameters(IDbCommand command, SqlMapper.Identity identity):
protected void AddParameters(IDbCommand command, SqlMapper.Identity identity)
{
var literals = SqlMapper.GetLiteralTokens(identity.sql);
if (templates != null)
{
//省略
}
foreach (var param in parameters.Values)
{
if (param.CameFromTemplate) continue;
var dbType = param.DbType;
var val = param.Value;
string name = Clean(param.Name);
var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter;
SqlMapper.ITypeHandler handler = null;
if (dbType == null && val != null && !isCustomQueryParameter)
{
#pragma warning disable 618
//未指定 dbType 時自動偵測
dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler);
#pragma warning disable 618
}
if (isCustomQueryParameter)
{
((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name);
}
else if (dbType == EnumerableMultiParameter)
{
#pragma warning disable 612, 618
SqlMapper.PackListParameters(command, name, val);
#pragma warning restore 612, 618
}
else
{
bool add = !command.Parameters.Contains(name);
IDbDataParameter p;
if (add)
{
//呼叫 CreateParameter() 建立該資料庫的參數物件
p = command.CreateParameter();
p.ParameterName = name;
}
else
{
p = (IDbDataParameter)command.Parameters[name];
}
p.Direction = param.ParameterDirection;
if (handler == null)
{
#pragma warning disable 0618
p.Value = SqlMapper.SanitizeParameterValue(val);
#pragma warning restore 0618
if (dbType != null && p.DbType != dbType)
{
p.DbType = dbType.Value; //指定型別
}
var s = val as string;
if (s?.Length <= DbString.DefaultLength)
{
p.Size = DbString.DefaultLength;
}
if (param.Size != null) p.Size = param.Size.Value;
if (param.Precision != null) p.Precision = param.Precision.Value;
if (param.Scale != null) p.Scale = param.Scale.Value;
}
else
{
if (dbType != null) p.DbType = dbType.Value; //指定型別
if (param.Size != null) p.Size = param.Size.Value;
if (param.Precision != null) p.Precision = param.Precision.Value;
if (param.Scale != null) p.Scale = param.Scale.Value;
handler.SetValue(p, val ?? DBNull.Value);
}
if (add)
{
command.Parameters.Add(p);
}
param.AttachedParam = p;
}
}
// note: most non-priveleged implementations would use: this.ReplaceLiterals(command);
if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals);
}
另外做了測試,SqlMapper.LookupDbType("张飞犇".GetType(), "T", true, out handler); 傳回的是 System.Data.DbType.String,handler = null,而就算自動偵測有誤,d.Add("T", "张飞犇", System.Data.DbType.String) 也能強制指定,不明白為什麼 DynamicParameters 處理 DbType.String 的結果會跟傳入匿名型別參數不同?
由於身處專案甘特圖的要徑上,後有人在等米下鍋,就先不追根究底,把擋路茶包搬開再說。在原始碼找到一條替代道路,當 value 型別為 SqlMapper.ICustomQueryParameter 時,有機會自訂決定 OracleParameter 的規則,於是我定義一個類別實作 ICustomQueryParameter:
public class SetNVarchar2 : SqlMapper.ICustomQueryParameter
{
private readonly string value;
public SetNVarchar2(string value)
{
this.value = value;
}
public void AddParameter(IDbCommand command, string name)
{
var oraCmd = command as OracleCommand;
oraCmd.Parameters.Add(name, OracleDbType.NVarchar2).Value = value;
}
}
加入參數部分改寫成:
var d = new DynamicParameters();
d.Add("I", 2);
d.Add("T", new SetNVarchar2("张飞犇"));
cn.Execute(cmdText, d);
花哈,問題就這麼解了,專案繼續前進。而學會 ICustomQueryParameter,未來要處理型別對映特殊情例,我又多了一件武器。
至於為什麼 DynamicParameters 行為不同的謎,就留待有空再另案調查了。
Issue of Dapper DynamicParameters failed to update Unicode string via ODP.NET and workaround.
Comments
# by Hung Yang
這幾天也不小心跌了一跤,在資料庫中存入::1的文字在varchar2欄位李,查詢的時候將他當成參數帶入查詢,結果回傳型別轉換錯誤,DAPPER用判斷成double數字,用DynamicParameters加上指定ANSI也一樣錯誤,還好還有舊的DataHelper類別能使用並轉成DataTable先救場。