各位前輩大家好
請問使用Dapper時
如果有一個類別是巢狀類別:
public class a {
public int id { get; set; }
public b b { get; set; }
public class b {
public int type { get; set; }
}
}
在使用的SQL語句有Join
string sql = @"SELECT * FROM A LEFT JOIN B ON A.id = B.id WHERE a.id = @id ";
最終需要的格式會像是:
{
"id": 1,
"b": {
"type": 1
}
}
請問在只需要單筆的狀況下該如何使用Dapper撈出這個格式呢?
在使用QueryFirstOrDefault時發現這個方法無法放入兩個類別去接值
現在使用的方式是
把sql拆兩段寫...
分別賦值塞回物件內這樣
想請教各位前輩會怎麼處理這一段呢?
謝謝!
被邀請答題。
這個需求可以使用 Query Multiple Mapping
解決,可以閱讀小弟寫的文章 : https://github.com/shps951023/Trace-Dapper.NET-Source-Code/blob/master/zh_tw.md#%E6%94%AF%E6%8F%B4dynamic-multi-mapping
截取段落:
在初期常變動表格結構或是一次性功能不想宣告Class,Dapper Multi Mapping也支援dynamic方式
void Main()
{
using (var ts = new TransactionScope())
using (var connection = Connection)
{
const string createSql = @"
create table Users (Id int, Name nvarchar(20))
create table Posts (Id int, OwnerId int, Content nvarchar(20))
insert Users values(1, N'小明')
insert Users values(2, N'小智')
insert Posts values(101, 1, N'小明第1天日記')
insert Posts values(102, 1, N'小明第2天日記')
insert Posts values(103, 2, N'小智第1天日記')
";
connection.Execute(createSql);
const string sql =
@"select * from Posts p
left join Users u on u.Id = p.OwnerId
Order by p.Id
";
var data = connection.Query<dynamic, dynamic, dynamic>(sql, (post, user) => { post.Owner = user; return post; }).ToList();
}
}
Split預設是用來切割主鍵,所以預設切割字串是Id
,假如當表格結構PK名稱為Id
可以省略參數,舉例
var result = cn.Query<Order,User,Order>(@"
select * from [order] T1
left join [User] T2 on T1.UserId = T2.ID
", (order, user) => {
order.User = user;
return order;
}
);
假如主鍵名稱是其他名稱,請指定splitOn字串名稱
,並且對應多個可以使用,
做區隔,舉例,添加商品表格做Join :
var result = cn.Query<Order,User,Item,Order>(@"
select * from [order] T1
left join [User] T2 on T1.UserId = T2.ID
left join [Item] T3 on T1.ItemId = T3.ID
"
,map : (order, user,item) => {
order.User = user;
order.Item = item;
return order;
}
,splitOn : "Id,Id"
);
這邊先以一個簡單Demo帶讀者了解Dapper Multi Mapping 概念
public static class MutipleMappingDemo
{
public static IEnumerable<TReturn> Query<T1, T2, TReturn>(this IDbConnection connection, string sql, Func<T1, T2, TReturn> map)
where T1 : Order, new()
where T2 : User, new() //這兩段where單純為了Demo方便
{
//1. 按照泛型類別參數數量建立對應數量的Mapping Func集合
var deserializers = new List<Func<IDataReader, object>>();
{
//2. Mapping Func建立邏輯跟Query Emit IL一樣
deserializers.Add((reader) =>
{
var newObj = new T1();
var value = default(object);
value = reader[0];
newObj.ID = value is DBNull ? 0 : (int)value;
value = reader[1];
newObj.OrderNo = value is DBNull ? null : (string)value;
return newObj;
});
deserializers.Add((reader) =>
{
var newObj = new T2();
var value = default(object);
value = reader[2];
newObj.ID = value is DBNull ? 0 : (int)value;
value = reader[4];
newObj.Name = value is DBNull ? null : (string)value;
return newObj;
});
}
using (var command = connection.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
//3. 呼叫使用者的Custom Mapping Func,其中參數由前面動態生成的Mapping Func而來
yield return map(deserializers[0](reader) as T1, deserializers[1](reader) as T2);
}
}
}
}
}
以上概念就是此方法的主要邏輯,接著講其他細節部分
Dapper為了強型別多類別Mapping
使用多組泛型參數方法
方式,這方式有個小缺點就是沒辦法動態調整
,需要以寫死方式來處理。
舉例,可以看到圖片GenerateMapper方法,依照泛型參數數量,寫死強轉型邏輯,這也是為何Multiple Query有最大組數限制,只能支持最多6組的原因。
泛型類別
來強型別
保存多類別的資料
override
的GetType方法,來客製泛型比較邏輯,避免造成跟Non Multi Query緩存衝突
。因為SplitOn分組基礎依賴於Select的順序
,所以順序一錯就有可能屬性值錯亂
情況。
舉例 : 假如上面例子的SQL改成以下,會發生User的ID變成Order的ID;Order的ID會變成User的ID。
select T2.[ID],T1.[OrderNo],T1.[UserID],T1.[ID],T2.[Name] from [order] T1
left join [User] T2 on T1.UserId = T2.ID
原因可以追究到Dapper的切割算法
倒序
方式處理欄位分組(GetNextSplit方法可以看到從DataReader Index大到小
查詢)
倒序
方式處理類別的Mapping Emit IL Func正序
,方便後面Call Func對應泛型使用
使用方式例子 :
using (var cn = Connection)
{
using (var gridReader = cn.QueryMultiple("select 1; select 2;"))
{
Console.WriteLine(gridReader.Read<int>()); //result : 1
Console.WriteLine(gridReader.Read<int>()); //result : 2
}
}
使用QueryMultiple優點 :
減少Reqeust次數
共用同一組Parameter參數
QueryMultiple的底層實作邏輯 :
DataReader NextResult
取得下一組查詢結果沒有
下一組查詢結果才會將DataReader釋放
緩存的算法多增加gridIndex判斷,主要對每個result mapping動作做一個緩存,Emit IL的邏輯跟Query一樣。
注意Read方法使用的是buffer = true = 返回結果直接ToList保存在內存,所以沒有延遲查詢特性。
Dapper 呼叫QueryMultiple方法時會將DataReader封裝在GridReader物件內,只有當最後一次Read
動作後才會回收DataReader
所以沒有讀取完
再開一個GridReader > Read會出現錯誤:已經開啟一個與這個 Command 相關的 DataReader,必須先將它關閉
。
要避免以上情況,可以改成using
區塊方式,運行完區塊代碼後就會自動釋放DataReader
using (var gridReader = cn.QueryMultiple("select 1; select 2;"))
{
//略..
}
有拜讀過大大的文章,最後確實是選擇mutiple的方式將sql拆成兩句
底層原理還需要更多時間慢慢理解,但確實是非常清楚!
另外想請教一下:
如果替換Insert時Parameter想用model的方式替換,但情況是除了Model之外還須使用其他變數的狀況
conn.excute(sql, dataModel, new { 某變數 = testInput })
這樣則無法使用,請問dapper有方式可以做到嗎?
等於想要除了用dataModel之外,還想再額外使用其他如果替換Insert時Parameter想用model的方式替換