前幾天分享我踩到 Dapper DynamicParameters + ODP.NET NVarchar2 產生中文亂碼的坑,有趣的是問題只發生在使用 DynamicParameters 傳值,若改用匿名型別則沒問題。推測是二者處理參數對映時邏輯有別,因時程壓力先找到 Workaround 避開問題繼續前進,留下未解謎團。

真相未能大白讓我耿耿於懷,週末花了點時間重新探勘,終於挖出問題根源,了卻一樁心願。(謎: 有那麼嚴重嗎?)

其實上次問題程式碼就跟我擦身而過,只是我沒認出來。在 DynamicParameters 原始碼 protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) 有以下這段:

IDbDataParameter p;
if (add)
{
    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;
    }

使用 command.CreateParamter() 建立 OracleParameter 後,若 DynamicParameters.Add() 時指定 dbType 指定型別「並且指定型別與 OracleParameter 現有 DbType 不同」時,將會重新設定 OracleParameter.DbType。因為有用 FixOdpNetDbTypeStringMapping(),OracleCommand.DbType 設成 DbType.String 理應對映成 OracleDbType.NVarchar2。為什麼 DynamicParameters.Add("T", "张飞犇", DbType.String) 也沒用?問題出在 CreateParamter() 建立的 OracleParameter.DbType 預設就是 DbType.String,依 (dbType != null && p.DbType != dbType) 邏輯不會執行 p.DbType = dbType.Value 重設。而 OracleCommand.DbType 屬性在 set 設定值時才會觸發 (OracleDbType)OraDb_DbTypeTable.dbTypeToOracleDbTypeMapping[(int)value]; 改變 OracleDbType 對映值,這就能解釋即使指定 DbType.String 也無法對映 OracleDbType.NVarchar2 的原因。

[Category("Data")]
[Description("")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public override DbType DbType
{
    get
    {
        if (!this.m_bSetDbType)
        {
            this.m_dbType = (DbType)OraDb_DbTypeTable.dbTypeToOracleDbTypeMapping[(int)this.m_oraDbType];
            this.m_bSetDbType = true;
        }
        return this.m_dbType;
    }
    set
    {
        if (value == DbType.Currency || value == DbType.SByte || value == DbType.UInt16 || value == DbType.UInt32 || value == DbType.UInt64 || value == DbType.VarNumeric || value == DbType.Guid || value == DbType.Xml || value == DbType.DateTime2)
        {
            throw new ArgumentException();
        }
        if (value < DbType.AnsiString || value > DbType.DateTimeOffset)
        {
            throw new ArgumentOutOfRangeException();
        }
        this.m_dbType = value;
        this.m_oraDbType = (OracleDbType)OraDb_DbTypeTable.dbTypeToOracleDbTypeMapping[(int)value];
        this.m_bSetDbType = true;
        this.m_modified = true;
        this.m_enumType = PrmEnumType.DBTYPE;
        this.m_bOracleDbTypeExSet = false;
    }
}

做個小實驗驗證,在下圖我們可以觀察到 OracleDbType 在設定 DbType.String 後,由 OracleDbType.Varchar2 變成 OracleDbType.NVarchar2 的過程:

經過這番分析,為何 DynamicParameters 無法對映 NVarchar2 便有了合理解釋。仔細評估過,我找不到比自訂 ICustomQueryParameter 更清晰簡潔的做法,而 OracleParameter 預設 DbType 與 OracleDbType 的不一致起因於我用 Hacking 手法解決 ODP.NET 問題,無法以此為由發 PR 請 Dapper 修改遇輯,故仍維持前篇文章建議的解法,以 ICustomQueryParameter 解決。

In this article, I found out the root cause of issue of Dapper DynamicParameters failing to update Unicode string via ODP.NET.


Comments

Be the first to post a comment

Post a comment