iT邦幫忙

0

Dapper 巢狀類別賦值

  • 分享至 

  • xImage

各位前輩大家好

請問使用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拆兩段寫...
分別賦值塞回物件內這樣

想請教各位前輩會怎麼處理這一段呢?
謝謝!

天黑 iT邦研究生 5 級 ‧ 2022-01-07 17:48:03 檢舉
https://dapper-tutorial.net/result-multi-mapping 看看這是不是你需要的
真酷我怎沒有這麼需求過....通常這種樣子的我都是拉回json才轉成class。還想不到什麼狀況會遇到你這種處境 @@
QuerySingle似乎沒有可以用的方法
如果要分兩次接型別,不曉得能不能夠把第一次查詢回來的表(含Join語法已經把欄位都查詢好)
再讓下面的語句用強型別去接
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 個回答

3
暐翰
iT邦大師 1 級 ‧ 2022-01-08 11:47:38

被邀請答題。

這個需求可以使用 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

截取段落:

支援dynamic 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();
  }
}

20191002023135.png

SplitOn區分類別Mapping組別

Split預設是用來切割主鍵,所以預設切割字串是Id,假如當表格結構PK名稱為Id可以省略參數,舉例 20191001151715.png

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"
);

14.Query Multi Mapping 底層原理

Multiple Mapping 底層原理

這邊先以一個簡單Demo帶讀者了解Dapper Multi Mapping 概念

  1. 按照泛型類別參數數量建立對應數量的Mapping Func集合
  2. Mapping Func建立邏輯跟Query Emit IL一樣
  3. 呼叫使用者的Custom Mapping Func,其中參數由前面動態生成的Mapping Func而來
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組的原因。
20191001173320.png

多類別泛型緩存算法

  • 這邊Dapper使用泛型類別強型別保存多類別的資料 20191001175139.png
  • 並配合繼承共用Identity大部分身分驗證邏輯
  • 提供可override的GetType方法,來客製泛型比較邏輯,避免造成跟Non Multi Query緩存衝突

20191001175600.png 20191001175707.png

Dapper Query Multi Mapping的Select順序很重要

因為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的切割算法

  1. 首先倒序方式處理欄位分組(GetNextSplit方法可以看到從DataReader Index大到小查詢) 20191002022109.png
  2. 接著倒序方式處理類別的Mapping Emit IL Func
  3. 最後反轉為正序,方便後面Call Func對應泛型使用 20191002021750.png 20191002022208.png 20191002022214.png

15.QueryMultiple 底層原理

使用方式例子 :

  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的底層實作邏輯 :

  1. 底層技術是ADO.NET - DataReader - MultipleResult
  2. QueryMultiple取得DataReader並封裝進GridReader
  3. 呼叫Read方法時才會建立Mapping動態方法,Emit IL動作跟Query方法一樣
  4. 接著使用ADO.NET技術呼叫DataReader NextResult取得下一組查詢結果
  5. 假如沒有下一組查詢結果才會將DataReader釋放

緩存算法

緩存的算法多增加gridIndex判斷,主要對每個result mapping動作做一個緩存,Emit IL的邏輯跟Query一樣。

20190930183038.png

沒有延遲查詢特性

注意Read方法使用的是buffer = true = 返回結果直接ToList保存在內存,所以沒有延遲查詢特性。

20190930183212.png 20190930183219.png

記得管理DataReader的釋放

Dapper 呼叫QueryMultiple方法時會將DataReader封裝在GridReader物件內,只有當最後一次Read動作後才會回收DataReader

20190930183447.png

所以沒有讀取完再開一個GridReader > Read會出現錯誤:已經開啟一個與這個 Command 相關的 DataReader,必須先將它關閉

20190930183532.png

要避免以上情況,可以改成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的方式替換

有另一個異想天開的問題是:
是否能夠有一個結果讓各個型別去接值呢?
可以在一開始查詢的時候就join各表,結果用個強行別去接

暐翰 iT邦大師 1 級 ‧ 2022-01-11 13:36:00 檢舉

但情況是除了Model之外還須使用其他變數的狀況

DynamicParameter or Dictionary<string,object>

是否能夠有一個結果讓各個型別去接值呢?

可以
QueryMultiple, Read<你指定的形别>

TO 暐翰
抱歉我沒有描述清楚,如果是一個結果(一個SELECT的結果)需要用兩種不同型態去接呢?
如果是Read<>,我的理解是會是下一個SELECT,如果只有一次SELECT,第二個Read<>就會出錯了

我要發表回答

立即登入回答