專案常遇到的需求:為指定資料保留修改歷程,以備稽核檢查或追查責任之用,使用機率不高且無統計或隨興查詢需求,不值得另開資料表。此時我偏好的做法是定義成List<HistoryRecord>,在資料表開一個NVARCHAR(MAX)保存其JSON內容,調閱時讀取JSON反序列化還原內容,足以滿足規格所需。
用個實例說明,假設資料物件定義如下:
publicclass HistoryRecord
{
public DateTime Time { get; set; }
publicstring User { get; set; }
publicstring Remark { get; set; }
}
publicclass ProjectItem
{
publicint Id { get; set; }
publicstring Name { get; set; }
public List<HistoryRecord> History { get; set; }
}
資料表設計如下:
CREATETABLE [dbo].[ProjectItem] (
[Id] INTNOTNULL,
[Name] NVARCHAR (64) NULL,
[History] NVARCHAR (MAX) NULL
);
資料庫存取部分我主要用Dapper實作,但問題來了,試著將List<HistoryRecord>當成History欄位的輸入參數:
var kernel = new ProjectItem()
{
Id = 1,
Name = "KernelModule",
History = new List<HistoryRecord>()
{
new HistoryRecord()
{
Time = new DateTime(2016, 7, 1),
User = "Jeffrey",
Remark = "Initial version"
},
new HistoryRecord()
{
Time = new DateTime(2016,7,11),
User = "Jeffrey",
Remark = "Refactoring"
}
}
};
cn.Execute("INSERT INTO ProjectItem VALUES(@Id, @Name, @History)", kernel);
卻冒出錯誤:The member of type DapperLab.Program+HistoryRecord cannot be used as a parameter value。Dapper不知道怎麼將List<HistoryRecord>轉成可以存入資料庫的內容。
針對這類需求,Dapper的解決方案是開放開發者自訂TypeHandler,指定型別該如何對應資料庫內容。實作方法很簡單,宣告一個類別繼承SqlMapper.TypeHandler<T>,提供兩個函式:Parse<T>()函式負責將資料庫內容轉成該型別,SetValue()函式將型別轉型後指定給IDbDataParameter.Value:
publicclass HistoryRecordListHandler : SqlMapper.TypeHandler<List<HistoryRecord>>
{
publicoverride List<HistoryRecord> Parse(objectvalue)
{
return JsonConvert.DeserializeObject<List<HistoryRecord>>((string)value);
}
publicoverridevoid SetValue(IDbDataParameter parameter, List<HistoryRecord> value)
{
parameter.Value = JsonConvert.SerializeObject(value);
}
}
接著,在執行cn.Execute()之前先透過SqlMapper.AddTypeHandler()註冊,指定由HistoryRecordListHandler負責處理List<HistoryRecord>資料轉換:
SqlMapper.AddTypeHandler<List<HistoryRecord>>(new HistoryRecordListHandler());
如此,Dapper就會將Historyh屬性JSON後寫入資料表,讀取時也能正確由JSON還原回List<HistoryRecord>,大功告成!
以上寫法可以再改良,HistoryRecordListHandler的核心邏輯可抽取成泛型,適用於所有要JSON化存入資料庫的型別,省去為每個要轉JSON型別撰寫專屬TypeConverter的困擾。
publicclass JsonConvertHandler<T> : SqlMapper.TypeHandler<T>
{
publicoverride T Parse(objectvalue)
{
return JsonConvert.DeserializeObject<T>((string)value);
}
publicoverridevoid SetValue(IDbDataParameter parameter, T value)
{
parameter.Value = JsonConvert.SerializeObject(value);
}
}
//...略...
SqlMapper.AddTypeHandler<List<HistoryRecord>>(new JsonConvertHandler<List<HistoryRecord>>());