野人獻曝,分享最近在寫的正式、測試台間的小規模資料搬移法。
情境是正式台跟測試台各有自己的資料庫,想將正式台某幾筆資料匯出,備份保存或是匯入測試台資料庫模擬測試;或是反過來,資料先在測試台輸入驗證無誤後要上線,希望將將輸入好的資料直接匯入正式台,省去在正式台重新登打的工夫。
這類情境用 Entity Framework 不難實現的,這篇介紹則介紹不用 EF 的做法。匯出匯入的前題是要有強型別的 Entity 型別,你可以手工宣告,也可借用 Visual Studio 強大的「貼上 JSON 做為類別」、「貼上 XML 做為類別」功能快速產生(參考:Visual Studio 的選擇性貼上,貼上Json作為類別 - 50懶 - 點部落)。
匯出時先用 .Query<EntityType>("SELECT * FROM Table") 產生 EntityType[],JSON 序列化後供使用者下載保存或匯入到另一套系統。實務上可透過壓縮提高傳輸與儲存效率(參考:程式範例:byte[] 不落地壓縮 ZIP 檔 - 黑暗執行緒)。
在匯入端則是先將 JSON 反序列化為 EntityType[],再來我選擇用 Reflection 自動產生 INSERT INTO 指令,再用 Dapper .Execute(InsertScript, EntityType[]) 一次塞入多筆資料,一行搞定。
程式範例如下:
public class Player
{
public int SeqNo { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public DateTime RegDate { get; set; }
public int Score { get; set; }
}
static void Main(string[] args)
{
string json = null;
//模擬由伺服器A匯出Player[],再轉成JSON
using (var cn = new SqlConnection(cs))
{
var data = cn.Query<Player>(
@"SELECT Id,Name,RegDate,Score FROM Players").ToArray();
json = JsonConvert.SerializeObject(data);
}
//補充:實務應用時可使用ZIP壓縮技術
//http://blog.darkthread.net/post-2018-08-14-zip-byte-array.aspx
//模擬上傳JSON到伺服器B,還原後INSERT進資料庫
var restored = JsonConvert.DeserializeObject<Player[]>(json);
var insertScript = GenerateInserScript(restored[0], "Players", "SeqNo".Split(','));
Console.WriteLine(insertScript);
using (var cn = new SqlConnection(cs))
{
//傳入Insert Script跟物件陣列,完成資料匯入
cn.Execute(insertScript, restored);
}
}
/// <summary>
/// 使用Reflection產生SQL INSERT腳本
/// </summary>
/// <param name="sample">樣本型別</param>
/// <param name="tableName">資料表名稱</param>
/// <param name="ignoredColNames">忽略欄位名稱</param>
/// <returns></returns>
static string GenerateInserScript(object sample, string tableName,
string[] ignoredColNames = null)
{
var sb = new StringBuilder();
//此處假設tableName由開發人員決定,不開放使用者輸入,否則要防範SQL Injection
sb.AppendLine("INSERT INTO " + tableName);
string[] props = sample.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(o => o.Name).ToArray();
props = props.Where(o =>
!o.StartsWith("_") && //排除_開頭及ignoredColNames列舉欄位名
(ignoredColNames == null || !ignoredColNames.Contains(o)))
.ToArray();
sb.AppendLine($"({string.Join(", ", props)})");
sb.AppendLine("VALUES");
sb.AppendLine($"({string.Join(", ", props.Select(o => "@" + o).ToArray())});");
return sb.ToString();
}
透過 Reflection 產生的 INSERT 語法如下,欄位名稱前面加上@就變成變數,剛好讓 Dapper 依欄位名稱自動對映,比想像中簡單很多吧! 這裡只設計可排除特定欄位(例如:自動跳號欄位)的設計,實務若有需要,可加入客製化邏輯。
以上是小量資料匯出匯入功能的簡單示範,我們下次見。