應該有很多人像我一樣,對LINQ的依賴已經到達"LINQ or Die!"(不LINQ,吾寧死)的地步,到了需要存取DB的場合,打死也不想再走ADO.NET + DataTable、DataRow的回頭路。不過,在專案引用EntityFramework或其他ORM解決方案(NHibernation、SubSonic...),固然嚴謹紮實,卻也多出額外工作--要依照Schema在專案定義Entity物件、資料庫變更時要記得同步更新Entity定義,遇到多TABLE JOIN查詢得另外宣告自訂類別承接查詢結果(我還為此寫過潛盾機)。對於要求嚴謹精準的中大型系統,這類準備工作屬無法避免的代價,但在一些力求快速輕巧的開發情境(例如: 轉檔工具、範例程式、單純但大量的報表需求...),所有對資料庫的存取(Table、View、Stored Procedure)需預先定義,還必須隨時保持與資料庫一致,引用EF之類的架構便顯笨重。
前陣子提到用TVP傳WHERE IN參數的做法,在FB專頁得到網友Lane Kuo的回饋(在此致謝),得知好物一枚 -- Dapper(英文原義是短小精悍,用過即知Dapper不負其名),一個精簡小巧的.NET ORM工具,不需在專案裡新增DB Table、View或Stored Procuedure定義,只要取得IDbConnection(SqlConnection、OracleConnection、MySqlConnection...都適用),就能立即享受接近EF、LINQ to SQL等ORM架構的便利,最重要的是能用LINQ把玩資料,這才是上流社會的程式寫法呀!
不囉嗦,打開NuGet就能找到它:
試用之後,感動到直起雞皮疙瘩,這就是我一直在尋找的,好吃又不黏牙的DB LINQ解決方案~
以下整理Dapper的特色:
第一點絕對要大推! 之前在公司推廣LINQ/EF,最常被挑戰的罩門 -- "過去不管再複雜的SELECT FROM WHERE,丟給ADO.NET就能拿到DataTable;弄了LINQ/EF之後,不定義Entity或自訂類別就收不到結果。幹! 你知道我的系統裡有多少個花式SELECT嗎?"
我一直覺得這是由傳統ADO.NET開發邁向LINQ/EF世界的最大阻礙,也動過腦筋想讓ExecuteStoreQuery<T>的T接受dynamic,而Dapper實現了!
using (var cn = new SqlConnection(cnStr))
{
//1) 不需要定義POCO物件,直接SELECT結果轉成.NET物件集合!(酷)
// 注意: 結果為IEnumerable<dynamic>,會喪失強型別優勢
//2) 可宣告及傳入具名參數
var list = cn.Query(
"SELECT * FROM Products WHERE CategoryID=@catg", new { catg = 2 });
foreach (var item in list)
{
Console.WriteLine("{0}.{1}({2})",
item.ProductID, item.ProductName, item.QuantityPerUnit);
}
}
另外,Dapper在SQL語法裡可使用具名參數(如@catg),不像ExecuteStoreQuery只能用{0}、{1},可讀性較佳。
執行結果:
3.Aniseed Syrup(12 - 550 ml bottles) 4.Chef Anton's Cajun Seasoning(48 - 6 oz jars) 5.Chef Anton's Gumbo Mix(36 boxes) 6.Grandma's Boysenberry Spread(12 - 8 oz jars) 8.Northwoods Cranberry Sauce(12 - 12 oz jars) 15.Genen Shouyu(24 - 250 ml bottles) 44.Gula Malacca(20 - 2 kg bags) 61.Sirop d'erable(24 - 500 ml bottles) 63.Vegie-spread(15 - 625 g jars) 65.Louisiana Fiery Hot Pepper Sauce(32 - 8 oz bottles) 66.Louisiana Hot Spiced Okra(24 - 8 oz jars) 77.Original Frankfurter grune Sose(12 boxes)
使用dynamic物件固然方便,但會喪失強型別在編譯時期的防錯優勢,因此Dapper當然也支援將查詢結果應對到自訂類別。此外,以下範例一併示範Dapper能直接將參數陣列展開成WHERE col IN (@arg1, @arg2, @arg3)的特異功能,相當方便。
publicclass SimpProduct
{
publicint ProductID { get; set; }
publicstring ProductName { get; set; }
}
privatestaticvoid Test()
{
using (var cn = new SqlConnection(cnStr))
{
//1) 將SELECT結果轉成指定的型別(屬性與欄位名稱要一致)
//2) 直接傳數字陣列作為WHERE IN比對參數
// =>自動轉成WHERE col in (@arg1,@arg2,@arg3)
var list = cn.Query<SimpProduct>(
"SELECT * FROM Products WHERE CategoryID IN @catgs",
new { catgs = newint[] { 1, 4 } });
foreach (var item in list)
{
Console.WriteLine("{0}.{1}",
item.ProductID, item.ProductName);
}
}
}
執行結果:
1.Chai 2.Chang 11.Queso Cabrales 12.Queso Manchego La Pastora 24.Guarana Fantastica 31.Gorgonzola Telino 32.Mascarpone Fabioli 33.Geitost 34.Sasquatch Ale 35.Steeleye Stout 38.Cote de Blaye 39.Chartreuse verte 43.Ipoh Coffee 59.Raclette Courdavault 60.Camembert Pierrot 67.Laughing Lumberjack Lager 69.Gudbrandsdalsost 70.Outback Lager 71.Flotemysost 72.Mozzarella di Giovanni 75.Rhonbrau Klosterbier 76.Lakkalikoori
除了查詢,Dapper提供.Execute()執行SQL資料更新,最特別的是它可以一次傳進多組參數,用不同參數重複執行同一SQL操作,批次作業時格外有用。
using (var cn = new SqlConnection(cnStr))
{
//1) 可執行SQL資料更新指令,支援參數
//2) 以陣列方式提供多組參數,可重複執行同一SQL指令
cn.Execute(@"INSERT INTO Region VALUES (@id, @desc)",
new[] {
new { id = 5, desc = "Taiwan" },
new { id = 6, desc = "Mars" }
});
}
Dapper還可以在命令中一次包含多組SELECT,透過QueryMultiple()後再以Read()或Read<T>分別取出查詢結果。
publicclass SimpCust
{
publicstring ContactName { get; set; }
publicstring ContactTitle { get; set; }
}
privatestaticvoid Test4()
{
using (var cn = new SqlConnection(cnStr))
{
//一次執行多組查詢,分別取回結果
var multi = cn.QueryMultiple(@"
SELECT * FROM Customers WHERE CustomerId = @id
SELECT * FROM Orders WHERE CustomerId = @id
", new { id = "ALFKI" });
var cust = multi.Read<SimpCust>().First();
Console.WriteLine("{0} / {1}", cust.ContactName, cust.ContactTitle);
var ords = multi.Read(); //取回IEnumerable<dynamic>
Console.WriteLine("Orders Count = {0}", ords.Count());
}
}
執行結果:
Maria Anders / Sales Representative Orders Count = 6
至於StoredProcedure,一樣可以透過Dapper Query()查詢及使用Execute()執行,直接取回SELECT結果或使用Output參數都難不倒它。
using (var cn = new SqlConnection(cnStr))
{
//呼叫StoredProcedure查詢資料
var res =
cn.Query("dbo.CustOrderHist", new { CustomerID = "ALFKI" },
commandType: CommandType.StoredProcedure);
foreach (var item in res)
{
Console.WriteLine("{0} = {1}", item.ProductName, item.Total);
}
//取回ReturnValue及Output參數
/*
CREATE PROCEDURE AddOne
@n INT, @r INT OUPUT
AS
BEGIN
SET @r = @n + 1
RETURN 1024
END
*/
var p = new DynamicParameters();
p.Add("@n", 1);
p.Add("@r", dbType: DbType.Int32,
direction: ParameterDirection.Output);
p.Add("@rtn", dbType: DbType.Int32,
direction: ParameterDirection.ReturnValue);
cn.Execute("dbo.AddOne", p,
commandType: CommandType.StoredProcedure);
Console.WriteLine("@r = {0}, return = {1}",
p.Get<int>("@r"), p.Get<int>("@rtn"));
}
}
執行結果:
Aniseed Syrup = 6 Chartreuse verte = 21 Escargots de Bourgogne = 40 Flotemysost = 20 Grandma's Boysenberry Spread = 16 Lakkalikoori = 15 Original Frankfurter grune Sose = 2 Raclette Courdavault = 15 Rossle Sauerkraut = 17 Spegesild = 2 Vegie-spread = 20 @r = 2, return = 1024
除了呼叫應用的便利性,Dapper很強調效能,在一些實測中明顯勝過EF及其他ORM架構。
【結論】
Dapper的輕巧犀利令人驚豔,讚嘆之餘頗有相見恨晚之感,不過現在知道也不算遲。未來中大型專案我想仍會維持預先定義Entity、Model、ViewModel,力求嚴謹分明的原則,但在一些需要巷戰搶灘近身肉博的場合,Dapper將會是我的好伙伴!