自從在 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先救場。

Post a comment