一個用資料表保存C# Model的常見問題,列舉型別屬性該怎麼處理?

例如有個BlogUser資料物件,包含Id、Name及Role三個屬性,其中Role是列舉,包含Admin、Editor、Blogger、Reader等項目。保存BlogUser的資料表設計如下,Role欄位定義為VARCHAR(8),目標為直接保存"Admin"、"Blogger"等字串內容,以期在SQL可使用WHERE Role = 'Blogger'進行篩選。

CREATE TABLE [dbo].[BlogUser] (
    [Id]   INT          NOT NULL,
    [Name] VARCHAR (16) NOT NULL,
    [Role] VARCHAR (8)  NOT NULL,
    CONSTRAINT [PK_BlogUser] PRIMARY KEY CLUSTERED ([Id] ASC)
);

使用Dapper執行資料更新及查詢的程式範例如下:

using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;
 
namespace DapperLab
{
    class Program
    {
        static string cnStr = "...由config取得連線字串(記得要加密),此處省略...";
 
        public enum Roles
        {
            Admin,
            Editor,
            Blogger,
            Reader
        }
 
        public class BlogUser
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public Roles Role { get; set; }
        }
 
        static void Main(string[] args)
        {
            using (var cn = new SqlConnection(cnStr))
            {
                var jeff = new BlogUser()
                {
                    Id = 1,
                    Name = "Jeffrey",
                    Role = Roles.Blogger
                };
                cn.Execute("INSERT INTO BlogUser VALUES(@Id, @Name, @Role)", jeff);
                var data = cn.Query<BlogUser>(
                    "SELECT * FROM BlogUser WHERE Id = @Id",
                    new { Id = 1 }).Single();
                Console.WriteLine("{0} {1} {2}", data.Id, data.Name, data.Role);
            }
        }
    }
}

測試結果Role列舉可以被寫入資料庫並正確還原,但Role欄位寫入的是Blogger列舉項目對應的數值"2"。

實測若在Role欄位存入'Blogger'也能正確還原回Roles.Blogger,但寫入時只能寫入數字讓人頭大。研究很久,一直試不出用列舉項目名稱取代數值寫入資料庫的做法。

昨天曾介紹過SqlMapper.TypeHandler<T>自訂轉換邏輯技巧,可惜無法適用列舉型別。由Dapper原始碼SqlMapper.cs邏輯,發現Dapper一旦偵測出IsEnum(),會無視TypeHandler設定直接使用Enum.ToObject()。

private static T Parse<T>(object value)
{
    if (value == null || value is DBNull) return default(T);
    if (value is T) return (T)value;
    var type = typeof(T);
    type = Nullable.GetUnderlyingType(type) ?? type;
    if (type.IsEnum())
    {
        if (value is float || value is double || value is decimal)
        {
            value = Convert.ChangeType(value, Enum.GetUnderlyingType(type), 
                    CultureInfo.InvariantCulture);
        }
        return (T)Enum.ToObject(type, value);
    }
    ITypeHandler handler;
    if (typeHandlers.TryGetValue(type, out handler))
    {
        return (T)handler.Parse(type, value);
    }
    return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
}

關於Enum該不該支援TypeHandler範圍,Github上有不少相關討論並無共識,預期短期內此一行為不會有所改變。(查看原始碼時,意外發現Dapper竟動用ILGenerator動態組裝MSIL處理欄位對應,相當變態,也難怪執行效能讓其他Reflection競爭者看不到車尾燈)

找不到克服之道也不想修改Dapper核心,最後我採取的做法是另外宣告一個RoleText屬性,提供以字串讀取及設定Role屬性的管道,其值與Role列舉100%對應,至於資料表欄位則改為RoleText VARCHAR(16)。程式範例如下:

        public class BlogUser
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public Roles Role { get; set; }
 
            [JsonIgnore]
            public string RoleText {
                get
                {
                    return Role.ToString();
                }
                set
                {
                    Roles res;
                    if (!Enum.TryParse((string)value, out res))
                    {
                        throw new ApplicationException(string.Format(
                            "Can't convert '{0}' to type [{1}]", value, typeof(Roles)));
                    }
                    Role = res;
                }
            }
        }

以上是我處理Dapper儲存列舉型別的經驗供參,大家如果知道其他妙計,歡迎回饋!


Comments

# by Dark.Guo

將參數改成自定義來調整,達到格式化 var param = new DynamicParameters(jeff); param.Add("Role", jeff.Role.ToString("G")); cn.Execute("INSERT INTO BlogUser VALUES(@Id, @Name, @Role)", param);

# by Jeffrey

to Dark.Guo,原來有 new DynamicParameters(obj) 這招,好用,謝謝分享。

# by yushiyau

@Dark.Guo 這是目前看到最好用的解法,感謝

Post a comment