深刻Dapper.NET源码 (文长)

目录

  1. 前言、目录、安装环境
  2. Dynamic Query 原理 Part1
  3. Dynamic Query 原理 Part2
  4. Strongly Typed Mapping 原理 Part1 : ADO.NET对比Dapper
  5. Strongly Typed Mapping 原理 Part2 : Reflection版本
  6. Strongly Typed Mapping 原理 Part3 : 动态创建方法重要概念「结果反推程式码」优化效率
  7. Strongly Typed Mapping 原理 Part4 : Expression版本
  8. Strongly Typed Mapping 原理 Part5 : Emit IL反创建C#代码
  9. Strongly Typed Mapping 原理 Part6 : Emit版本
  10. Dapper 效率快关键之一 : Cache 缓存原理
  11. 错误SQL字串拼接方式,会致使效率慢、内存泄漏
  12. Dapper SQL正确字串拼接方式 : Literal Replacement
  13. Query Multi Mapping 使用方式
  14. Query Multi Mapping 底层原理
  15. QueryMultiple 底层原理
  16. TypeHandler 自订Mapping逻辑使用、底层逻辑
  17. CommandBehavior的细节处理
  18. Parameter 参数化底层原理
  19. IN 多集合参数化底层原理
  20. DynamicParameter 底层原理、自订实做
  21. 单次、屡次 Execute 底层原理
  22. ExecuteScalar应用
  23. 总结

 
 

1.前言、目录、安装环境

通过业界前辈、StackOverflow多年推广,「Dapper搭配Entity Framework」成为一种功能强大的组合,它知足「安全、方便、高效、好维护」需求。html

但目前中文网路文章,虽然有不少关于Dapper的文章但都停留在如何使用,没人系统性解说底层原理。因此有了此篇「深刻Dapper源码」想带你们进入Dapper底层,了解Dapper的精美细节设计、高效原理,并学起来实际应用在工做当中。git


创建Dapper Debug环境

  1. Dapper Github 首页 Clone最新版本到本身本机端
  2. 创建.NET Core Console专案
    20191003173131.png
  3. 须要安装NuGet SqlClient套件、添加Dapper Project Reference
    20191003173438.png
  4. 下中断点运行就能够Runtime查看逻辑
    20191003215021.png

我的环境

  • 数据库 : MSSQLLocalDB
  • Visaul Studio版本 : 2019
  • LINQ Pad 5 版本
  • Dapper版本 : V2.0.30
  • 反编译 : ILSpy

 
 

2.Dynamic Query 原理 Part1

在前期开发阶段由于表格结构还在调整阶段,或是不值得额外宣告类别轻量需求,使用Dapper dynamic Query能够节省下来回修改class属性的时间。当表格稳定下来后使用POCO生成器快速生成Class转成强型别维护。github

为什么Dapper能够如此方便,支援dynamic?

追溯Query方法源码能够发现两个重点面试

  1. 实体类别实际上是DapperRow再隐性转型为dynamic。
    20191003180501.png
  2. DapperRow继承IDynamicMetaObjectProvider而且实做对应方法。

20191003044133.png

此段逻辑我这边作一个简化版本的Dapper dynamic Query让读者了解转换逻辑 :算法

  1. 创建dynamic类别变量,实体类别是ExpandoObject
  2. 由于有继承关系能够转型为IDictionary<string, object>
  3. 使用DataReader使用GetName取得栏位名称,借由栏位index取得值,并将二者分别添加进Dictionary看成key跟value。
  4. 由于ExpandoObject有实做IDynamicMetaObjectProvider介面能够转换成dynamic
public static class DemoExtension
{
  public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql)
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
      {
        while (reader.Read())
        {
          yield return reader.CastToDynamic();
        }
      }
    }
  }
  
  private static dynamic CastToDynamic(this IDataReader reader)
  {
    dynamic e = new ExpandoObject();
    var d = e as IDictionary<string,object>;
    for (int i = 0; i < reader.FieldCount; i++)
      d.Add(reader.GetName(i),reader[i]);
    return e;
  }
}

20191003044145.png

 
 

3.Dynamic Query 原理 Part2

有了前面简单ExpandoObject Dynamic Query例子的概念后,接着进到底层来了解Dapper如何细节处理,为什么要自订义DynamicMetaObjectProvider。sql

首先掌握Dynamic Query流程逻辑 :

假设使用下面代码数据库

using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
{
    var result = cn.Query("select N'暐翰' Name,26 Age").First();
    Console.WriteLine(result.Name);
}

取值的过程会是 : 创建动态Func > 保存在缓存 > 使用result.Name > 转成呼叫 ((DapperRow)result)["Name"] > 从DapperTable.Values阵列中以"Name"栏位对应的Index取值express

接着查看源码GetDapperRowDeserializer方法,它掌管dynamic如何运行的逻辑,并动态创建成Func给上层API呼叫、缓存重复利用。
20191003190836.pngc#

此段Func逻辑 :缓存

  1. DapperTable虽然是方法内的局部变量,可是被生成的Func引用,因此不会被GC一直保存在内存内重复利用。
    20191003182219.png

  2. 由于是dynamic不须要考虑类别Mapping,这边直接使用GetValue(index)向数据库取值
var values = new object[select栏位数量];
for (int i = 0; i < values.Length; i++)
{
    object val = r.GetValue(i);
    values[i] = val is DBNull ? null : val;
}
  1. 将资料保存到DapperRow内
public DapperRow(DapperTable table, object[] values)
{
    this.table = table ?? throw new ArgumentNullException(nameof(table));
    this.values = values ?? throw new ArgumentNullException(nameof(values));
}
  1. DapperRow 继承 IDynamicMetaObjectProvider 并实做 GetMetaObject 方法,实做逻辑是返回DapperRowMetaObject物件。
private sealed partial class DapperRow : System.Dynamic.IDynamicMetaObjectProvider
{
    DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this);
    }
}
  1. DapperRowMetaObject主要功能是定义行为,借由override BindSetMember、BindGetMember方法,Dapper定义了Get、Set的行为分别使用IDictionary<string, object> - GetItem方法DapperRow - SetValue方法
    20191003210351.png
    20191003210547.png

  2. 最后Dapper利用DataReader的栏位顺序性,先利用栏位名称取得Index,再利用Index跟Values取得值

20191003211448.png

为什么要继承IDictionary<string,object>?

能够思考一个问题 : 在DapperRowMetaObject能够自行定义Get跟Set行为,那么不使用Dictionary - GetItem方法,改用其余方式,是否表明不须要继承IDictionary<string,object>?

Dapper这样作的缘由之一跟开放原则有关,DapperTable、DapperRow都是底层实做类别,基于开放封闭原则不该该开放给使用者,因此设为private权限。

private class DapperTable{/*略*/}
private class DapperRow :IDictionary<string, object>, IReadOnlyDictionary<string, object>,System.Dynamic.IDynamicMetaObjectProvider{/*略*/}

那么使用者想要知道栏位名称怎么办?
由于DapperRow实做IDictionary因此能够向上转型为IDictionary<string, object>,利用它为公开介面特性取得栏位资料。

public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable{/*略*/}

举个例子,笔者有作一个小工具HtmlTableHelper就是利用这特性,自动将Dapper Dynamic Query转成Table Html,如如下代码跟图片

using (var cn = "Your Connection")
{
  var sourceData = cn.Query(@"select 'ITWeiHan' Name,25 Age,'M' Gender");
  var tablehtml = sourceData.ToHtmlTable(); //Result : <table><thead><tr><th>Name</th><th>Age</th><th>Gender</th></tr></thead><tbody><tr><td>ITWeiHan</td><td>25</td><td>M</td></tr></tbody></table>
}

20191003212846.png

 
 

4. Strongly Typed Mapping 原理 Part1 : ADO.NET对比Dapper

接下来是Dapper关键功能 Strongly Typed Mapping,由于难度高,这边会切分红多篇来解说。

第一篇先以ADO.NET DataReader GetItem By Index跟Dapper Strongly Typed Query对比,查看二者IL的差别,了解Dapper Query Mapping的主要逻辑。

有了逻辑后,如何实做,我这边依序用三个技术 :Reflection、Expression、Emit 从头实做三个版本Query方法来让读者渐进式了解。


ADO.NET对比Dapper

首先使用如下代码来追踪Dapper Query逻辑

class Program
{
  static void Main(string[] args)
  {
    using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
    {
      var result = cn.Query<User>("select N'暐翰' Name , 25 Age").First();
      Console.WriteLine(result.Name);
      Console.WriteLine(result.Age);
    }
  }
}

public class User
{
  public string Name { get; set; }
  public int Age { get; set; }
}

这边须要重点来看Dapper.SqlMapper.GenerateDeserializerFromMap方法,它负责Mapping的逻辑,能够看到里面大量使用Emit IL技术。

20191004012713.png

要了解这段IL逻辑,个人方式 :「不该该直接进到细节,而是先查看完整生成的IL」,至于如何查看,这边须要先准备 il-visualizer 开源工具,它能够在Runtime查看DynamicMethod生成的IL。

它预设支持vs 201五、2017,假如跟我同样使用vs2019的读者,须要注意

  1. 须要手动解压缩到
    %USERPROFILE%\Documents\Visual Studio 2019路径下面
    20191004005622.png
  2. .netstandard2.0专案,须要创建netstandard2.0并解压缩到该资料夹
    20191003044307.png

最后重开visaul studio并debug运行,进到GetTypeDeserializerImpl方法,对DynamicMethod点击放大镜 > 選擇IL visualizer > 查看Runtime生成的IL代码
20191010120715.png
20191003044320.png

能够得出如下IL

IL_0000: ldc.i4.0   
IL_0001: stloc.0    
IL_0002: newobj     Void .ctor()/Demo.User
IL_0007: stloc.1    
IL_0008: ldloc.1    
IL_0009: dup        
IL_000a: ldc.i4.0   
IL_000b: stloc.0    
IL_000c: ldarg.0    
IL_000d: ldc.i4.0   
IL_000e: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0013: dup        
IL_0014: stloc.2    
IL_0015: dup        
IL_0016: isinst     System.DBNull
IL_001b: brtrue.s   IL_0029
IL_001d: unbox.any  System.String
IL_0022: callvirt   Void set_Name(System.String)/Demo.User
IL_0027: br.s       IL_002b
IL_0029: pop        
IL_002a: pop        
IL_002b: dup        
IL_002c: ldc.i4.1   
IL_002d: stloc.0    
IL_002e: ldarg.0    
IL_002f: ldc.i4.1   
IL_0030: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0035: dup        
IL_0036: stloc.2    
IL_0037: dup        
IL_0038: isinst     System.DBNull
IL_003d: brtrue.s   IL_004b
IL_003f: unbox.any  System.Int32
IL_0044: callvirt   Void set_Age(Int32)/Demo.User
IL_0049: br.s       IL_004d
IL_004b: pop        
IL_004c: pop        
IL_004d: stloc.1    
IL_004e: leave      IL_0060
IL_0053: ldloc.0    
IL_0054: ldarg.0    
IL_0055: ldloc.2    
IL_0056: call       Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_005b: leave      IL_0060
IL_0060: ldloc.1    
IL_0061: ret

要了解这段IL以前须要先了解ADO.NET DataReader快速读取资料方式会使用GetItem By Index方式,如如下代码

public static class DemoExtension
{
  private static User CastToUser(this IDataReader reader)
  {
    var user = new User();
    var value = reader[0];
    if(!(value is System.DBNull))
      user.Name = (string)value;
    var value = reader[1];
    if(!(value is System.DBNull))
      user.Age = (int)value;      
    return user;
  }

  public static IEnumerable<User> Query<T>(this IDbConnection cnn, string sql)
  {
    if (cnn.State == ConnectionState.Closed) cnn.Open();
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
        while (reader.Read())
          yield return reader.CastToUser();
    }
  }
}

接着查看此Demo - CastToUser方法生成的IL代码

DemoExtension.CastToUser:
IL_0000:  nop         
IL_0001:  newobj      User..ctor
IL_0006:  stloc.0     // user
IL_0007:  ldarg.0     
IL_0008:  ldc.i4.0    
IL_0009:  callvirt    System.Data.IDataRecord.get_Item
IL_000E:  stloc.1     // value
IL_000F:  ldloc.1     // value
IL_0010:  isinst      System.DBNull
IL_0015:  ldnull      
IL_0016:  cgt.un      
IL_0018:  ldc.i4.0    
IL_0019:  ceq         
IL_001B:  stloc.2     
IL_001C:  ldloc.2     
IL_001D:  brfalse.s   IL_002C
IL_001F:  ldloc.0     // user
IL_0020:  ldloc.1     // value
IL_0021:  castclass   System.String
IL_0026:  callvirt    User.set_Name
IL_002B:  nop         
IL_002C:  ldarg.0     
IL_002D:  ldc.i4.1    
IL_002E:  callvirt    System.Data.IDataRecord.get_Item
IL_0033:  stloc.1     // value
IL_0034:  ldloc.1     // value
IL_0035:  isinst      System.DBNull
IL_003A:  ldnull      
IL_003B:  cgt.un      
IL_003D:  ldc.i4.0    
IL_003E:  ceq         
IL_0040:  stloc.3     
IL_0041:  ldloc.3     
IL_0042:  brfalse.s   IL_0051
IL_0044:  ldloc.0     // user
IL_0045:  ldloc.1     // value
IL_0046:  unbox.any   System.Int32
IL_004B:  callvirt    User.set_Age
IL_0050:  nop         
IL_0051:  ldloc.0     // user
IL_0052:  stloc.s     04 
IL_0054:  br.s        IL_0056
IL_0056:  ldloc.s     04 
IL_0058:  ret

跟Dapper生成的IL比对能够发现大体是同样的(差别部分后面会讲解),表明二者在运行的逻辑、效率上都会是差很少的,这也是为什么Dapper效率接近原生ADO.NET的缘由之一。

 
 

5. Strongly Typed Mapping 原理 Part2 : Reflection版本

在前面ADO.NET Mapping例子能够发现严重问题「没办法多类别共用方法,每新增一个类别就须要重写代码」。要解决这个问题,能够写一个共用方法在Runtime时期针对不一样的类别作不一样的逻辑处理。

实做方式作主要有三种Reflection、Expression、Emit,这边首先介绍最简单方式:「Reflection」,我这边会使用反射方式从零模拟Query写代码,让读者初步了解动态处理概念。(假若有经验的读者能够跳过本篇)

逻辑 :

  1. 使用泛型传递动态类别
  2. 使用泛型的条件约束new()达到动态创建物件
  3. DataReader须要使用属性字串名称当Key,可使用Reflection取得动态类别的属性名称,在借由DataReader this[string parameter]取得数据库资料
  4. 使用PropertyInfo.SetValue方式动态将数据库资料赋予物件

最后获得如下代码 :

public static class DemoExtension
{
  public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
        while (reader.Read())
          yield return reader.CastToType<T>();
    }
  }

  //1.使用泛型传递动态类别
  private static T CastToType<T>(this IDataReader reader) where T : new()
  {
    //2.使用泛型的条件约束new()达到动态创建物件
    var instance = new T();

    //3.DataReader须要使用属性字串名称当Key,可使用Reflection取得动态类别的属性名称,在借由DataReader this[string parameter]取得数据库资料
    var type = typeof(T);
    var props = type.GetProperties();
    foreach (var p in props)
    {
      var val = reader[p.Name];

      //4.使用PropertyInfo.SetValue方式动态将数据库资料赋予物件
      if( !(val is System.DBNull) )  
        p.SetValue(instance, val);
    }

    return instance;
  }
}

Reflection版本优势是代码简单,但它有如下问题

  1. 不该该重复属性查询,没用到就要忽略
    举例 : 假如类别有N个属性,SQL指查询3个栏位,土炮ORM每次PropertyInfo foreach仍是N次不是3次。而Dapper在Emit IL当中特别优化此段逻辑 : 「查多少用多少,不浪费」(这段以后讲解)。
    https://ithelp.ithome.com.tw/upload/images/20191003/20105988Y7jmgF76Wd.png
    https://ithelp.ithome.com.tw/upload/images/20191003/20105988nHZMb3Copc.png
  2. 效率问题 :
  • 反射效率会比较慢,这点以后会介绍解决方式 : 「查表法 + 动态创建方法」以空间换取时间。
  • 使用字串Key取值会多呼叫了GetOrdinal方法,能够查看MSDN官方解释,效率比Index取值差
    https://ithelp.ithome.com.tw/upload/images/20191003/20105988ABufu55xes.png
    https://ithelp.ithome.com.tw/upload/images/20191003/20105988TqMlMbAIls.png

 
 

6.Strongly Typed Mapping 原理 Part3 : 动态创建方法重要概念「结果反推程式码」优化效率

接着使用Expression来解决Reflection版本问题,主要是利用Expression特性 : 「能够在Runtime时期动态创建方法」来解决问题。

在这以前须要先有一个重要概念 : 「从结果反推最简洁代码」优化效率,举个例子 : 之前初学程式时一个经典题目「打印正三角型星星」作出一个长度为3的正三角,常见做法会是回圈+递回方式

void Main()
{
  Print(3,0);
}

static void Print(int length, int spaceLength)
{
  if (length < 0)
    return;
  else
    Print(length - 1, spaceLength + 1);
  for (int i = 0; i < spaceLength; i++)
    Console.Write(" ");
  for (int i = 0; i < length; i++)
    Console.Write("* ");
  Console.WriteLine("");
}

但其实这个题目在已经知道长度的状况下,能够被改为如下代码

Console.WriteLine("  * ");
Console.WriteLine(" * * ");
Console.WriteLine("* * * ");

这个概念很重要,由于是从结果反推代码,因此逻辑直接、效率快,而Dapper就是使用此概念来动态创建方法。

举例 : 假设有一段代码以下,咱们能够从结果得出

  • User Class的Name属性对应Reader Index 0 、类别是String 、 预设值是null
  • User Class的Age属性对应Reader Index 1 、类别是int 、 预设值是0
void Main()
{
  using (var cn = Connection)
  {
    var result = cn.Query<User>("select N'暐翰' Name,26 Age").First();
  }
}

class User
{
  public string Name { get; set; }
  public int Age { get; set; }
}

假如系统能帮忙生成如下逻辑方法,那么效率会是最好的

User 动态方法(IDataReader reader)
{
  var user = new User();
  var value = reader[0];
  if( !(value is System.DBNull) )
    user.Name = (string)value;
  value = reader[1];
  if( !(value is System.DBNull) )
    user.Age = (int)value;  
  return user;
}

另外上面例子能够看出对Dapper来讲SQL Select对应Class属性顺序很重要,因此后面会讲解Dapper在缓存的算法特别针对此优化。

 
 

7.Strongly Typed Mapping 原理 Part4 : Expression版本

有了前面的逻辑,就着使用Expression实做动态创建方法。

为什么先使用 Expression 实做而不是 Emit ?

除了有能力动态创建方法,相比Emit有如下优势 :

  • 可读性好,可用熟悉的关键字,像是变量Variable对应Expression.Variable、创建物件New对应Expression.New
    https://ithelp.ithome.com.tw/upload/images/20190920/20105988rkSmaILTw7.png
  • 方便Runtime Debug,能够在Debug模式下看到Expression对应逻辑代码
    https://ithelp.ithome.com.tw/upload/images/20190920/201059882EODD9OdnD.png
    https://ithelp.ithome.com.tw/upload/images/20190920/201059882gSYyfUduS.png

因此特别适合介绍动态方法创建,但Expression相比Emit没法做一些细节操做,这点会在后面Emit讲解到。

改写Expression版本

逻辑 :

  1. 取得sql select全部栏位名称
  2. 取得mapping类别的属性资料 > 将index,sql栏位,class属性资料作好对应封装在一个变量内方便后面使用
  3. 动态创建方法 : 从数据库Reader按照顺序读取咱们要的资料,其中代码逻辑 :
User 动态方法(IDataReader reader)
{
  var user = new User();
  var value = reader[0];
  if( !(value is System.DBNull) )
    user.Name = (string)value;
  value = reader[1];
  if( !(value is System.DBNull) )
    user.Age = (int)value;  
  return user;
}

最后得出如下Exprssion版本代码

public static class DemoExtension
{
  public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
      {
        var func = CreateMappingFunction(reader, typeof(T));
        while (reader.Read())
        {
          var result = func(reader as DbDataReader);
          yield return result is T ? (T)result : default(T);
        }

      }
    }
  }

  private static Func<DbDataReader, object> CreateMappingFunction(IDataReader reader, Type type)
  {
    //1. 取得sql select全部栏位名称
    var names = Enumerable.Range(0, reader.FieldCount).Select(index => reader.GetName(index)).ToArray();

    //2. 取得mapping类别的属性资料 >  将index,sql栏位,class属性资料作好对应封装在一个变量内方便后面使用
    var props = type.GetProperties().ToList();
    var members = names.Select((columnName, index) =>
    {
      var property = props.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal))
      ?? props.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase));
      return new
      {
        index,
        columnName,
        property
      };
    });

    //3. 动态创建方法 : 从数据库Reader按照顺序读取咱们要的资料
    /*方法逻辑 : 
      User 动态方法(IDataReader reader)
      {
        var user = new User();
        var value = reader[0];
        if( !(value is System.DBNull) )
          user.Name = (string)value;
        value = reader[1];
        if( !(value is System.DBNull) )
          user.Age = (int)value;  
        return user;
      }
    */
    var exBodys = new List<Expression>();
    {
      // 方法(IDataReader reader)
      var exParam = Expression.Parameter(typeof(DbDataReader), "reader");

      // Mapping类别 物件 = new Mapping类别();
      var exVar = Expression.Variable(type, "mappingObj");
      var exNew = Expression.New(type);
      {
        exBodys.Add(Expression.Assign(exVar, exNew));
      }

      // var value = defalut(object);
      var exValueVar = Expression.Variable(typeof(object), "value");
      {
        exBodys.Add(Expression.Assign(exValueVar, Expression.Constant(null)));
      }


      var getItemMethod = typeof(DbDataReader).GetMethods().Where(w => w.Name == "get_Item")
        .First(w => w.GetParameters().First().ParameterType == typeof(int));
      foreach (var m in members)
      {
        //reader[0]
        var exCall = Expression.Call(
          exParam, getItemMethod,
          Expression.Constant(m.index)
        );

        // value = reader[0];
        exBodys.Add(Expression.Assign(exValueVar, exCall));

        //user.Name = (string)value;
        var exProp = Expression.Property(exVar, m.property.Name);
        var exConvert = Expression.Convert(exValueVar, m.property.PropertyType); //(string)value
        var exPropAssign = Expression.Assign(exProp, exConvert);

        //if ( !(value is System.DBNull))
        //    (string)value
        var exIfThenElse = Expression.IfThen(
          Expression.Not(Expression.TypeIs(exValueVar, typeof(System.DBNull)))
          , exPropAssign
        );

        exBodys.Add(exIfThenElse);
      }


      // return user;  
      exBodys.Add(exVar);

      // Compiler Expression 
      var lambda = Expression.Lambda<Func<DbDataReader, object>>(
        Expression.Block(
          new[] { exVar, exValueVar },
          exBodys
        ), exParam
      );

      return lambda.Compile();
    }
  }
}

查询效果图 :
20191004205645.png

最后查看Expression.Lambda > DebugView(注意是非公开属性)验证代码 :

.Lambda #Lambda1<System.Func`2[System.Data.Common.DbDataReader,System.Object]>(System.Data.Common.DbDataReader $reader) {
    .Block(
        UserQuery+User $mappingObj,
        System.Object $value) {
        $mappingObj = .New UserQuery+User();
        $value = null;
        $value = .Call $reader.get_Item(0);
        .If (
            !($value .Is System.DBNull)
        ) {
            $mappingObj.Name = (System.String)$value
        } .Else {
            .Default(System.Void)
        };
        $value = .Call $reader.get_Item(1);
        .If (
            !($value .Is System.DBNull)
        ) {
            $mappingObj.Age = (System.Int32)$value
        } .Else {
            .Default(System.Void)
        };
        $mappingObj
    }
}

20191005035640.png

 
 

8. Strongly Typed Mapping 原理 Part5 : Emit IL反创建C#代码

有了前面Expression版本概念后,接着能够进到Dapper底层最核心的技术 : Emit。

首先要有个概念,MSIL(CIL)目的是给JIT编译器看的,因此可读性会不好、难Debug,但比起Expression来讲能够作到更细节的逻辑操做。

在实际环境开发使用Emit,通常会先写好C#代码后 > 反编译查看IL > 使用Emit创建动态方法,举例 :

1.首先创建一个简单打印例子 :

void SyaHello()
{
  Console.WriteLine("Hello World");  
}

2.反编译查看IL

SyaHello:
IL_0000:  nop         
IL_0001:  ldstr       "Hello World"
IL_0006:  call        System.Console.WriteLine
IL_000B:  nop         
IL_000C:  ret

3.使用DynamicMethod + Emit创建动态方法

void Main()
{
  // 1. 创建 void 方法()
  DynamicMethod methodbuilder = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(),typeof(void),null);

  // 2. 创建方法Body内容,借由Emit
  var il = methodbuilder.GetILGenerator();
  il.Emit(OpCodes.Ldstr, "Hello World");
  Type[] types = new Type[1]
  {
    typeof(string)
  };
  MethodInfo method = typeof(Console).GetMethod("WriteLine", types);
  il.Emit(OpCodes.Call,method);
  il.Emit(OpCodes.Ret);
  
  // 3. 转换指定类型的Func or Action
  var action = (Action)methodbuilder.CreateDelegate(typeof(Action));
  
  action(); 
}

https://ithelp.ithome.com.tw/upload/images/20190924/20105988bD9GSXyjNt.png

可是对已经写好的专案来讲就不是这样流程了,开发者不必定会好心的告诉你当初设计的逻辑,因此接着讨论此问题。

若是像是Dapper只有Emit IL没有C# Source Code专案怎么办?

个人解决方式是 : 「既然只有Runtime才能知道IL,那么将IL保存成静态档案再反编译查看」

这边可使用MethodBuild + Save方法将IL保存成静态exe档案 > 反编译查看,但须要特别注意

  1. 请对应好参数跟返回类别,不然会编译错误。
  2. netstandard不支援此方式,Dapper须要使用region if 指定版本来作区分,不然不能使用,如图片
    20191004230125.png

代码以下 :

//使用MethodBuilder查看别人已经写好的Emit IL
  //1. 创建MethodBuilder
  AppDomain ad = AppDomain.CurrentDomain;
  AssemblyName am = new AssemblyName();
  am.Name = "TestAsm";
  AssemblyBuilder ab = ad.DefineDynamicAssembly(am, AssemblyBuilderAccess.Save);
  ModuleBuilder mb = ab.DefineDynamicModule("Testmod", "TestAsm.exe");
  TypeBuilder tb = mb.DefineType("TestType", TypeAttributes.Public);
  MethodBuilder dm = tb.DefineMethod("TestMeThod", MethodAttributes.Public |
  MethodAttributes.Static, type, new[] { typeof(IDataReader) });
  ab.SetEntryPoint(dm);

  // 2. 填入IL代码
  //..略

  // 3. 生成静态档案
  tb.CreateType();
  ab.Save("TestAsm.exe");

接着使用此方式在GetTypeDeserializerImpl方法反编译Dapper Query Mapping IL,能够得出C#代码 :

public static User TestMeThod(IDataReader P_0)
{
  int index = 0;
  User user = new User();
  object value = default(object);
  try
  {
    User user2 = user;
    index = 0;
    object obj = value = P_0[0];
    if (!(obj is DBNull))
    {
      user2.Name = (string)obj;
    }
    index = 1;
    object obj2 = value = P_0[1];
    if (!(obj2 is DBNull))
    {
      user2.Age = (int)obj2;
    }
    user = user2;
    return user;
  }
  catch (Exception ex)
  {
    SqlMapper.ThrowDataException(ex, index, P_0, value);
    return user;
  }
}

20191004230548.png

有了C#代码后再来了解Emit逻辑会快不少,接着就能够进到Emit版本Query实做部分。

 
 

9.Strongly Typed Mapping 原理 Part6 : Emit版本

如下代码是Emit版本,我把C#对应IL部分都写在注解。

public static class DemoExtension
{
  public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
      {
        var func = GetTypeDeserializerImpl(typeof(T), reader);

        while (reader.Read())
        {
          var result = func(reader as DbDataReader);
          yield return result is T ? (T)result : default(T);
        }
      }

    }
  }

  private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
  {
    var returnType = type.IsValueType ? typeof(object) : type;

    var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(IDataReader) }, type, true);
    var il = dm.GetILGenerator();

    //C# : User user = new User();
    //IL : 
    //IL_0001:  newobj      
    //IL_0006:  stloc.0         
    var constructor = returnType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0]; //这边简化成只会有预设constructor
    il.Emit(OpCodes.Newobj, constructor);
    var returnValueLocal = il.DeclareLocal(type);
    il.Emit(OpCodes.Stloc, returnValueLocal); //User user = new User();

    // C# : 
    //object value = default(object);
    // IL :
    //IL_0007: ldnull
    //IL_0008:  stloc.1     // value  
    var valueLoacl = il.DeclareLocal(typeof(object));
    il.Emit(OpCodes.Ldnull);
    il.Emit(OpCodes.Stloc, valueLoacl);
    
    
    int index = startBound;
    var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int))
            .Select(p => p.GetGetMethod()).First();

    foreach (var p in type.GetProperties())
    {
      //C# : value = P_0[0];
      //IL:
      //IL_0009:  ldarg.0      
      //IL_000A: ldc.i4.0
      //IL_000B: callvirt System.Data.IDataRecord.get_Item
      //IL_0010:  stloc.1     // value        
      il.Emit(OpCodes.Ldarg_0); //取得reader参数
      EmitInt32(il, index);
      il.Emit(OpCodes.Callvirt, getItem);
      il.Emit(OpCodes.Stloc, valueLoacl);


      //C#: if (!(value is DBNull)) user.Name = (string)value;
      //IL:
      // IL_0011:  ldloc.1     // value
      // IL_0012:  isinst      System.DBNull
      // IL_0017:  ldnull      
      // IL_0018:  cgt.un      
      // IL_001A:  ldc.i4.0   
      // IL_001B:  ceq         
      // IL_001D:  stloc.2    
      // IL_001E:  ldloc.2     
      // IL_001F:  brfalse.s   IL_002E
      // IL_0021:  ldloc.0     // user
      // IL_0022:  ldloc.1     // value
      // IL_0023:  castclass   System.String
      // IL_0028:  callvirt    UserQuery+User.set_Name      
      il.Emit(OpCodes.Ldloc, valueLoacl);
      il.Emit(OpCodes.Isinst, typeof(System.DBNull));
      il.Emit(OpCodes.Ldnull);
      
      var tmpLoacl = il.DeclareLocal(typeof(int));
      il.Emit(OpCodes.Cgt_Un);
      il.Emit(OpCodes.Ldc_I4_0);
      il.Emit(OpCodes.Ceq);
      
      il.Emit(OpCodes.Stloc,tmpLoacl);
      il.Emit(OpCodes.Ldloc,tmpLoacl);
      
      
      var labelFalse = il.DefineLabel();
      il.Emit(OpCodes.Brfalse_S,labelFalse);
      il.Emit(OpCodes.Ldloc, returnValueLocal);
      il.Emit(OpCodes.Ldloc, valueLoacl);
      if (p.PropertyType.IsValueType)
        il.Emit(OpCodes.Unbox_Any, p.PropertyType);
      else
        il.Emit(OpCodes.Castclass, p.PropertyType);
      il.Emit(OpCodes.Callvirt, p.SetMethod);
      
      il.MarkLabel(labelFalse);

      index++;
    }

    // IL_0053:  ldloc.0     // user
    // IL_0054:  stloc.s     04  //不须要
    // IL_0056:  br.s        IL_0058
    // IL_0058:  ldloc.s     04  //不须要
    // IL_005A:  ret         
    il.Emit(OpCodes.Ldloc, returnValueLocal);
    il.Emit(OpCodes.Ret);

    var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(IDataReader), returnType);
    return (Func<IDataReader, object>)dm.CreateDelegate(funcType);
  }

  private static void EmitInt32(ILGenerator il, int value)
  {
    switch (value)
    {
      case -1: il.Emit(OpCodes.Ldc_I4_M1); break;
      case 0: il.Emit(OpCodes.Ldc_I4_0); break;
      case 1: il.Emit(OpCodes.Ldc_I4_1); break;
      case 2: il.Emit(OpCodes.Ldc_I4_2); break;
      case 3: il.Emit(OpCodes.Ldc_I4_3); break;
      case 4: il.Emit(OpCodes.Ldc_I4_4); break;
      case 5: il.Emit(OpCodes.Ldc_I4_5); break;
      case 6: il.Emit(OpCodes.Ldc_I4_6); break;
      case 7: il.Emit(OpCodes.Ldc_I4_7); break;
      case 8: il.Emit(OpCodes.Ldc_I4_8); break;
      default:
        if (value >= -128 && value <= 127)
        {
          il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
        }
        else
        {
          il.Emit(OpCodes.Ldc_I4, value);
        }
        break;
    }
  }
}

这边Emit的细节概念很是的多,这边没法所有都讲解,先挑出重要概念讲解

Emit Label

在Emit if/else须要使用Label定位,告知编译器条件为true/false时要跳到哪一个位子,举例 : 「boolean转整数」,假设要简单将Boolean转换成Int,C#代码能够用「若是是True返回1不然返回0」逻辑来写:

public static int BoolToInt(bool input) => input ? 1 : 0;

当转成Emit写法的时候,须要如下逻辑 :

  1. 考虑Label动态定位问题
  2. 先要创建好Label让Brtrue_S知道符合条件时要去哪一个Label位子 (注意,这时候Label位子还没肯定)
  3. 继续按顺序由上而下创建IL
  4. 等到了符合条件要运行区块的前一行,使用MarkLabel方法标记Label的位子

最后写出的C# Emit代码 :

public class Program
{
  public static void Main(string[] args)
  {
    var func = CreateFunc();
    Console.WriteLine(func(true)); //1
    Console.WriteLine(func(false)); //0
  }

  static Func<bool, int> CreateFunc()
  {
    var dm = new DynamicMethod("Test" + Guid.NewGuid().ToString(), typeof(int), new[] { typeof(bool) });

    var il = dm.GetILGenerator();
    var labelTrue = il.DefineLabel();

    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Brtrue_S, labelTrue);
    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ret);
    il.MarkLabel(labelTrue);
    il.Emit(OpCodes.Ldc_I4_1);
    il.Emit(OpCodes.Ret);

    var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(bool), typeof(int));
    return (Func<bool, int>)dm.CreateDelegate(funcType);
  }
}

这边能够发现Emit版本
优势 :

  1. 能作更多细节的操做
  2. 由于细节颗粒度小,能够优化的效率更好

缺点 :

  1. 难以Debug
  2. 可读性差
  3. 代码量变大、复杂度增长

接着来看Dapper做者的建议,如今通常专案当中没有必要使用Emit,使用Expression + Func/Action已经能够解决大部分动态方法的需求,尤为是Expression支援Block等方法状况。连结 c# - What's faster: expression trees or manually emitting IL

20190927163441.png

话虽如此,但有一些厉害的开源专案就是使用Emit管理细节,若是想看懂它们,就须要基础的Emit IL概念

 
 

10.Dapper 效率快关键之一 : Cache 缓存原理

为什么Dapper能够这么快?

前面介绍到动态使用 Emit IL 创建 ADO.NET Mapping 方法,但单就这功能没法让 Dapper 被称为轻量ORM效率之王。

由于动态创建方法是须要成本、并耗费时间的动做,单纯使用反而会拖慢速度。但当配合 Cache 后就不同,将创建好的方法保存在 Cache 内,能够用『空间换取时间』概念加快查询的效率,也就是俗称查表法

接着追踪Dapper源码,此次须要特别关注的是QueryImpl方法下的Identity、GetCacheInfo
https://ithelp.ithome.com.tw/upload/images/20191005/20105988cCwaS7ejnY.png

Identity、GetCacheInfo

Identity主要封装各缓存的比较Key属性 :

  • sql : 区分不一样SQL字串
  • type : 区分Mapping类别
  • commandType : 负责区分不一样数据库
  • gridIndex : 主用用在QueryMultiple,后面讲解。
  • connectionString : 主要区分同数据库厂商可是不一样DB状况
  • parametersType : 主要区分参数类别
  • typeCount : 主要用在Multi Query多映射,须要搭配override GetType方法,后面讲解

接着搭配GetCacheInfo方法内Dapper使用的缓存类别ConcurrentDictionary<Identity, CacheInfo>,使用TryGetValue方法时会去先比对HashCode接着比对Equals特性,如图片源码。
https://ithelp.ithome.com.tw/upload/images/20191005/20105988tOgZiBCwly.png

将Key类别Identity借由override Equals方法实现缓存比较算法,能够看到如下Dapper实做逻辑,只要一个属性不同就会创建一个新的动态方法、缓存。

public bool Equals(Identity other)
{
  if (ReferenceEquals(this, other)) return true;
  if (ReferenceEquals(other, null)) return false;

  int typeCount;
  return gridIndex == other.gridIndex
    && type == other.type
    && sql == other.sql
    && commandType == other.commandType
    && connectionStringComparer.Equals(connectionString, other.connectionString)
    && parametersType == other.parametersType
    && (typeCount = TypeCount) == other.TypeCount
    && (typeCount == 0 || TypesEqual(this, other, typeCount));
}

以此概念拿以前Emit版本修改为一个简单Cache Demo让读者感觉:

public class Identity
{
  public string sql { get; set; }
  public CommandType? commandType { get; set; }
  public string connectionString { get; set; }
  public Type type { get; set; }
  public Type parametersType { get; set; }
  public Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType)
  {
    this.sql = sql;
    this.commandType = commandType;
    this.connectionString = connectionString;
    this.type = type;
    this.parametersType = parametersType;
    unchecked
    {
      hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this
      hashCode = (hashCode * 23) + commandType.GetHashCode();
      hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0);
      hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0);
      hashCode = (hashCode * 23) + (connectionString == null ? 0 : StringComparer.Ordinal.GetHashCode(connectionString));
      hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0);
    }
  }

  public readonly int hashCode;
  public override int GetHashCode() => hashCode;
  
  public override bool Equals(object obj) => Equals(obj as Identity);
  public bool Equals(Identity other)
  {
    if (ReferenceEquals(this, other)) return true;
    if (ReferenceEquals(other, null)) return false;

    return type == other.type
      && sql == other.sql
      && commandType == other.commandType
      && StringComparer.Ordinal.Equals(connectionString, other.connectionString)
      && parametersType == other.parametersType;
  }
}

public static class DemoExtension
{
  private static readonly Dictionary<Identity, Func<DbDataReader, object>> readers = new Dictionary<Identity, Func<DbDataReader, object>>();

  public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql,object param=null) where T : new()
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
      {
        var identity = new Identity(command.CommandText, command.CommandType, cnn.ConnectionString, typeof(T), param?.GetType());
        
        // 2. 若是cache有资料就使用,没有资料就动态创建方法并保存在缓存内
        if (!readers.TryGetValue(identity, out Func<DbDataReader, object> func))
        {
          //动态创建方法
          func = GetTypeDeserializerImpl(typeof(T), reader);
          readers[identity] = func;
          Console.WriteLine("没有缓存,创建动态方法放进缓存");
        }else{
          Console.WriteLine("使用缓存");
        }


        // 3. 呼叫生成的方法by reader,读取资料回传
        while (reader.Read())
        {
          var result = func(reader as DbDataReader);
          yield return result is T ? (T)result : default(T);
        }
      }

    }
  }

  private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
  {
    //..略
  }
}

效果图 :
https://ithelp.ithome.com.tw/upload/images/20191005/20105988mKud6Ejzqe.png

 
 

11.错误SQL字串拼接方式,会致使效率慢、内存泄漏

了解实做逻辑后,接着延伸一个Dapper使用的重要观念,SQL字串为缓存重要Key值之一,假如不一样的SQL字串,Dapper会为此创建新的动态方法、缓存,因此使用不当状况下就算使用StringBuilder也会形成效率慢、内存泄漏问题

https://ithelp.ithome.com.tw/upload/images/20190916/20105988lAFBAWbhS6.png

至于为什么要以SQL字串当其中一个关键Key,而不是单纯使用Mapping类别的Handle,其中缘由之一是跟查询栏位顺序有关,在前面有讲到,Dapper使用「结果反推程式码」方式创建动态方法,表明说顺序跟资料都必需要是固定的,避免SQL Select栏位顺序不同又使用同一组动态方法,会有A栏位值给B属性错值大问题。

最直接解决方式,对每一个不一样SQL字串创建不一样的动态方法,并保存在不一样的缓存。

举例,如下代码只是简单的查询动做,查看Dapper Cache数量却达到999999个,如Gif动画显示

using (var cn = new SqlConnection(@"connectionString"))
{
    for (int i = 0; i < 999999; i++)
    {
        var guid = Guid.NewGuid();
        for (int i2 = 0; i2 < 2; i2++)
        {
            var result = cn.Query<User>($"select '{guid}' ").First();
        }  
    }
}

zeiAPVJ

要避免此问题,只须要保持一个原则重复利用SQL字串,而最简单方式就是参数化, 举例 : 将上述代码改为如下代码,缓存数量降为1,达到重复利用目的 :

using (var cn = new SqlConnection(@"connectionString"))
{
    for (int i = 0; i < 999999; i++)
    {
        var guid = Guid.NewGuid();
        for (int i2 = 0; i2 < 2; i2++)
        {
            var result = cn.Query<User>($"select @guid ",new { guid}).First();
        }  
    }
}

4IR5M47

 
 

12.Dapper SQL正确字串拼接方式 : Literal Replacement

假如遇到必要拼接SQL字串需求的状况下,举例 : 有时候值使用字串拼接会比不使用参数化效率好,特别是该栏位值只会有几种固定值

这时候Dapper可使用Literal Replacements功能,使用方式 : 将要拼接的值字串以{=属性名称}取代,并将值保存在Parameter参数内,举例 :

void Main()
{
  using (var cn = Connection)
  {
    var result = cn.Query("select N'暐翰' Name,26 Age,{=VipLevel} VipLevel", new User{ VipLevel = 1}).First();
  }
}

为何Literal Replacement能够避免缓存问题

首先追踪源码GetCacheInfo下GetLiteralTokens方法,能够发现Dapper在创建缓存以前会抓取SQL字串内符合{=变量名称}规格的资料。

private static readonly Regex literalTokens = new Regex(@"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled);
internal static IList<LiteralToken> GetLiteralTokens(string sql)
{
  if (string.IsNullOrEmpty(sql)) return LiteralToken.None;
  if (!literalTokens.IsMatch(sql)) return LiteralToken.None;

  var matches = literalTokens.Matches(sql);
  var found = new HashSet<string>(StringComparer.Ordinal);
  List<LiteralToken> list = new List<LiteralToken>(matches.Count);
  foreach (Match match in matches)
  {
    string token = match.Value;
    if (found.Add(match.Value))
    {
      list.Add(new LiteralToken(token, match.Groups[1].Value));
    }
  }
  return list.Count == 0 ? LiteralToken.None : list;
}

接着在CreateParamInfoGenerator方法生成Parameter参数化动态方法,此段方法IL以下 :

IL_0000: ldarg.1    
IL_0001: castclass  <>f__AnonymousType1`1[System.Int32]
IL_0006: stloc.0    
IL_0007: ldarg.0    
IL_0008: callvirt   System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand
IL_000d: pop        
IL_000e: ldarg.0    
IL_000f: ldarg.0    
IL_0010: callvirt   System.String get_CommandText()/System.Data.IDbCommand
IL_0015: ldstr      "{=VipLevel}"
IL_001a: ldloc.0    
IL_001b: callvirt   Int32 get_VipLevel()/<>f__AnonymousType1`1[System.Int32]
IL_0020: stloc.1    
IL_0021: ldloca.s   V_1

IL_0023: call       System.Globalization.CultureInfo get_InvariantCulture()/System.Globalization.CultureInfo
IL_0028: call       System.String ToString(System.IFormatProvider)/System.Int32
IL_002d: callvirt   System.String Replace(System.String, System.String)/System.String
IL_0032: callvirt   Void set_CommandText(System.String)/System.Data.IDbCommand
IL_0037: ret

接着再生成Mapping动态方法,要了解此段逻辑我这边作一个模拟例子方便读者理解 :

public static class DbExtension
{
  public static IEnumerable<User> Query(this DbConnection cnn, string sql, User parameter)
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      CommandLiteralReplace(command, parameter);
      using (var reader = command.ExecuteReader())
        while (reader.Read())
          yield return Mapping(reader);
    }
  }

  private static void CommandLiteralReplace(IDbCommand cmd, User parameter)
  {
    cmd.CommandText = cmd.CommandText.Replace("{=VipLevel}", parameter.VipLevel.ToString(System.Globalization.CultureInfo.InvariantCulture));
  }

  private static User Mapping(IDataReader reader)
  {
    var user = new User();
    var value = default(object);
    value = reader[0];
    if(!(value is System.DBNull))
      user.Name = (string)value;
    value = reader[1];
    if (!(value is System.DBNull))
      user.Age = (int)value;
    value = reader[2];
    if (!(value is System.DBNull))
      user.VipLevel = (int)value;
    return user;
  }
}

看完以上例子,能够发现Dapper Literal Replacements底层原理就是字串取代,一样属于字串拼接方式,为什么能够避免缓存问题?

这是由于取代的时机点在SetParameter动态方法内,因此Cache的SQL Key是没有变更过的,能够重复利用一样的SQL字串、缓存。

也由于是字串取代方式,因此只支持基本Value类别,假如使用String类别系统会告知The type String is not supported for SQL literals.,避免SQL Injection问题。

 
 

13.Query Multi Mapping 使用方式

接着讲解Dapper Multi Mapping(多对应)实做跟底层逻辑,毕竟工做当中不可能都是一对一律念。

使用方式 :

  • 须要本身编写Mapping逻辑,使用方式 : Query<Func逻辑>(SQL,Parameter,Mapping逻辑Func)
  • 须要指定泛型参数类别,规则为Query<Func第一个类别,Func第二个类别,..以此类推,Func最后返回类别> (最多支持六组泛型参数)
  • 指定切割栏位名称,预设使用ID,假如不同须要特别指定 (这段后面特别讲解)
  • 以上顺序都是由左至右

举例 : 有订单(Order)跟会员(User)表格,关系是一对多关系,一个会员能够有多个订单,如下是C# Demo代码 :

void Main()
{
  using (var ts = new TransactionScope())
  using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
  {
    cn.Execute(@"
      CREATE TABLE [User]([ID] int, [Name] nvarchar(10));
      INSERT INTO [User]([ID], [Name])VALUES(1, N'大雄'),(2, N'小明');

      CREATE TABLE [Order]([ID] int, [OrderNo] varchar(13), [UserID] int);
      INSERT INTO [Order]([ID], [OrderNo], [UserID])VALUES(1, 'SO20190900001', 1),(2, 'SO20190900002', 1),(3, 'SO20190900003', 2),(4, 'SO20190900004', 2);
    ");

    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;
      }
    );
    
    ts.Dispose();
  }
}

public class Order
{
  public int ID { get; set; }  
  public string OrderNo { get; set; }  
  public User User { get; set; }  
}

public class User
{
  public int ID { get; set; }
  public string Name { get; set; }
}

20191001145311.png


支援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;"))
{
  //略..
}

闲话 :

感受Dapper GridReader好像有机会能够实做是否有NextResult方法,这样就能够配合while方法一次读取完多组查询资料,等以后有空来想一想有没有机会作成。

概念代码 :

public static class DbExtension
{
  public static IEnumerable<IEnumerable<dynamic>> GetMultipleResult(this IDbConnection cn,string sql, object paramters)
  {
    using (var reader = cn.QueryMultiple(sql,paramters))
    {
      while(reader.NextResult())
      {
        yield return reader.Read();
      }
    }
  }
}

 
 

16.TypeHandler 自订Mapping逻辑使用、底层逻辑

遇到想要客制某些属性Mapping逻辑时,在Dapper可使用TypeHandler

使用方式 :

  • 创建类别继承SqlMapper.TypeHandler
  • 将要客制的类别指定给泛型,e.g : JsonTypeHandler<客制类别> : SqlMapper.TypeHandler<客制类别>
  • 查询的逻辑使用override实做Parse方法,增删改逻辑实做SetValue方法
  • 假如多个类别Parse、SetValue共用一样逻辑,能够将实做类别改成泛型方式,客制类别在AddTypeHandler时指定就能够,能够避免创建一堆类别,e.g : JsonTypeHandler<T> : SqlMapper.TypeHandler<T> where T : class

举例 :
想要特定属性成员在数据库保存Json,在AP端自动转成对应Class类别,这时候可使用SqlMapper.AddTypeHandler<继承实做TypeHandler的类别>

如下例子是User资料变动时会自动在Log栏位纪录变动动做。

public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T> 
  where T : class
{
  public override T Parse(object value)
  {
    return JsonConvert.DeserializeObject<T>((string)value);
  }

  public override void SetValue(IDbDataParameter parameter, T value)
  {
    parameter.Value = JsonConvert.SerializeObject(value);
  }
}

public void Main()
{
  SqlMapper.AddTypeHandler(new JsonTypeHandler<List<Log>>()); 

  using (var ts = new TransactionScope())
  using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
  {

    cn.Execute("create table [User] (Name nvarchar(200),Age int,Level int,Logs nvarchar(max))");

    var user = new User()
    {
      Name = "暐翰",
      Age = 26,
      Level = 1,
      Logs = new List<Log>() {
        new Log(){Time=DateTime.Now,Remark="CreateUser"}
      }
    };

    //新增资料
    {
      cn.Execute("insert into [User] (Name,Age,Level,Logs) values (@Name,@Age,@Level,@Logs);", user);

      var result = cn.Query("select * from [User]");
      Console.WriteLine(result);
    }

    //升级Level动做
    {
      user.Level = 9;
      user.Logs.Add(new Log() {Remark="UpdateLevel"});
      cn.Execute("update [User] set Level = @Level,Logs = @Logs where Name = @Name", user);
      var result = cn.Query("select * from [User]");
      Console.WriteLine(result);
    }

    ts.Dispose();

  }
}

public class User
{
  public string Name { get; set; }
  public int Age { get; set; }
  public int Level { get; set; }
  public List<Log> Logs { get; set; }

}
public class Log
{
  public DateTime Time { get; set; } = DateTime.Now;
  public string Remark { get; set; }
}

效果图 :
20190929231937.png


接着追踪TypeHandler源码逻辑,须要分两个部份来追踪 : SetValue,Parse

SetValue底层原理

  1. AddTypeHandlerImpl方法管理缓存的添加
  2. 在CreateParamInfoGenerator方法Emit创建动态AddParameter方法时,假如该Mapping类别TypeHandler缓存内有资料,Emit添加呼叫SetValue方法动做。
if (handler != null)
{
  il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache<int>.SetValue))); // stack is now [parameters] [[parameters]] [parameter]
}
  1. 在Runtime呼叫AddParameters方法时会使用LookupDbType,判断是否有自订TypeHandler
    20191006151723.png
    20191006151614.png
  2. 接着将创建好的Parameter传给自订TypeHandler.SetValue方法
    20191006151901.png

最后查看IL转成的C#代码

public static void TestMeThod(IDbCommand P_0, object P_1)
    {
        User user = (User)P_1;
        IDataParameterCollection parameters = P_0.Parameters;
        //略...
        IDbDataParameter dbDataParameter3 = P_0.CreateParameter();
        dbDataParameter3.ParameterName = "Logs";
        dbDataParameter3.Direction = ParameterDirection.Input;
        SqlMapper.TypeHandlerCache<List<Log>>.SetValue(dbDataParameter3, ((object)user.Logs) ?? ((object)DBNull.Value));
        parameters.Add(dbDataParameter3);
        //略...
    }

能够发现生成的Emit IL会去从TypeHandlerCache取得咱们实做的TypeHandler,接着呼叫实做SetValue方法运行设定的逻辑,而且TypeHandlerCache特别使用泛型类别依照不一样泛型以Singleton方式保存不一样handler,这样有如下优势 :

  1. 只要传递泛型类别参数就能够取得同一个handler避免重复创建物件
  2. 由于是泛型类别,取handler时能够避免了反射动做,提高效率

https://ithelp.ithome.com.tw/upload/images/20190929/20105988x970H6xWXC.png
https://ithelp.ithome.com.tw/upload/images/20190929/20105988S7VZLLXLZo.png
https://ithelp.ithome.com.tw/upload/images/20190929/20105988Q1mWkL0GP6.png


Parse对应底层原理

主要逻辑是在GenerateDeserializerFromMap方法Emit创建动态Mapping方法时,假如判断TypeHandler缓存有资料,以Parse方法取代本来的Set属性动做。
https://ithelp.ithome.com.tw/upload/images/20190930/20105988JvCw5z207s.png

查看动态Mapping方法生成的IL代码 :

IL_0000: ldc.i4.0   
IL_0001: stloc.0    
IL_0002: newobj     Void .ctor()/Demo.User
IL_0007: stloc.1    
IL_0008: ldloc.1    
IL_0009: dup        
IL_000a: ldc.i4.0   
IL_000b: stloc.0    
IL_000c: ldarg.0    
IL_000d: ldc.i4.0   
IL_000e: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0013: dup        
IL_0014: stloc.2    
IL_0015: dup        
IL_0016: isinst     System.DBNull
IL_001b: brtrue.s   IL_0029
IL_001d: unbox.any  System.String
IL_0022: callvirt   Void set_Name(System.String)/Demo.User
IL_0027: br.s       IL_002b
IL_0029: pop        
IL_002a: pop        
IL_002b: dup        
IL_002c: ldc.i4.1   
IL_002d: stloc.0    
IL_002e: ldarg.0    
IL_002f: ldc.i4.1   
IL_0030: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0035: dup        
IL_0036: stloc.2    
IL_0037: dup        
IL_0038: isinst     System.DBNull
IL_003d: brtrue.s   IL_004b
IL_003f: unbox.any  System.Int32
IL_0044: callvirt   Void set_Age(Int32)/Demo.User
IL_0049: br.s       IL_004d
IL_004b: pop        
IL_004c: pop        
IL_004d: dup        
IL_004e: ldc.i4.2   
IL_004f: stloc.0    
IL_0050: ldarg.0    
IL_0051: ldc.i4.2   
IL_0052: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0057: dup        
IL_0058: stloc.2    
IL_0059: dup        
IL_005a: isinst     System.DBNull
IL_005f: brtrue.s   IL_006d
IL_0061: unbox.any  System.Int32
IL_0066: callvirt   Void set_Level(Int32)/Demo.User
IL_006b: br.s       IL_006f
IL_006d: pop        
IL_006e: pop        
IL_006f: dup        
IL_0070: ldc.i4.3   
IL_0071: stloc.0    
IL_0072: ldarg.0    
IL_0073: ldc.i4.3   
IL_0074: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0079: dup        
IL_007a: stloc.2    
IL_007b: dup        
IL_007c: isinst     System.DBNull
IL_0081: brtrue.s   IL_008f
IL_0083: call       System.Collections.Generic.List`1[Demo.Log] Parse(System.Object)/Dapper.SqlMapper+TypeHandlerCache`1[System.Collections.Generic.List`1[Demo.Log]]
IL_0088: callvirt   Void set_Logs(System.Collections.Generic.List`1[Demo.Log])/Demo.User
IL_008d: br.s       IL_0091
IL_008f: pop        
IL_0090: pop        
IL_0091: stloc.1    
IL_0092: leave      IL_00a4
IL_0097: ldloc.0    
IL_0098: ldarg.0    
IL_0099: ldloc.2    
IL_009a: call       Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_009f: leave      IL_00a4
IL_00a4: ldloc.1    
IL_00a5: ret

转成C#代码来验证 :

public static User TestMeThod(IDataReader P_0)
  {
    int index = 0;
    User user = new User();
    object value = default(object);
    try
    {
      User user2 = user;
      index = 0;
      object obj = value = P_0[0];
      //..略
      index = 3;
      object obj4 = value = P_0[3];
      if (!(obj4 is DBNull))
      {
        user2.Logs = SqlMapper.TypeHandlerCache<List<Log>>.Parse(obj4);
      }
      user = user2;
      return user;
    }
    catch (Exception ex)
    {
      SqlMapper.ThrowDataException(ex, index, P_0, value);
      return user;
    }
  }

 
 

17. CommandBehavior的细节处理

这篇将带读者了解Dapper如何在底层利用CommandBehavior优化查询效率,如何选择正确Behavior在特定时机。

我这边整理了各方法对应的Behavior表格 :

方法 Behavior
Query CommandBehavior.SequentialAccess & CommandBehavior.SingleResult
QueryFirst CommandBehavior.SequentialAccess & CommandBehavior.SingleResult & CommandBehavior.SingleRow
QueryFirstOrDefault CommandBehavior.SequentialAccess & CommandBehavior.SingleResult & CommandBehavior.SingleRow
QuerySingle CommandBehavior.SingleResult & CommandBehavior.SequentialAccess
QuerySingleOrDefault CommandBehavior.SingleResult & CommandBehavior.SequentialAccess
QueryMultiple CommandBehavior.SequentialAccess

SequentialAccess、SingleResult优化逻辑

首先能够看到每一个方法都使用CommandBehavior.SequentialAccess,该标签主要功能 使DataReader顺序读取行和列,行和列不缓冲,读取一列后,它会从内存中删除。,有如下优势 :

  1. 可按顺序分次读取资源,避免二进制大资源一次性读取到内存,尤为是Blob或是Clob会配合GetBytes 或 GetChars 方法限制缓冲区大小,微软官方也特别标注注意 :
    20191003014421.png
  2. 实际环境测试,能够加快查询效率

但它却不是DataReader的预设行为,系统预设是CommandBehavior.Default
20191003015853.png
CommandBehavior.Default有着如下特性 :

  1. 可传回多个结果集(Multi Result)
  2. 一次性读取行资料到内存

这两个特性跟生产环境状况差满多,毕竟大多时刻是只须要一组结果集配合有限的内存,因此除了SequentialAccess外Dapper还特别在大多方法使用了CommandBehavior.SingleResult,知足只需一组结果就好避免浪费资源。

这段还有一段细节的处理,查看源码能够发现除了标记SingleResult外,Dapper还特别加上一段代码在结尾while (reader.NextResult()){},而不是直接Return(如图片)

20191003021109.png

早些前我有特别发Issue(连结#1210)询问过做者,这边是回答 : 主要避免忽略错误,像是在DataReader提前关闭状况


QueryFirst搭配SingleRow,

有时候咱们会遇到select top 1知道只会读取一行资料的状况,这时候可使用QueryFirst。它使用CommandBehavior.SingleRow能够避免浪费资源只读取一行资料。

另外能够发现此段除了while (reader.NextResult()){}外还有while (reader.Read()) {},一样是避免忽略错误,这是一些公司自行土炮ORM会忽略的地方。
20191003024206.png

与QuerySingle之间的差异

二者差异在QuerySingle没有使用CommandBehavior.SingleRow,至于为什么没有使用,是由于须要有多行资料才能判断是否不符合条件并抛出Exception告知使用者

这段有一个特别好玩小技巧能够学,错误处理直接沿用对应LINQ的Exception,举例:超过一行资料错误,使用new int[2].Single(),这样不用另外维护Exceptiono类别,还能够拥有i18N多国语言化。
20191003025631.png
20191003025334.png

 
 

18.Parameter 参数化底层原理

接着进到Dapper的另外一个关键功能 : 「Parameter 参数化」

主要逻辑 :
GetCacheInfo检查是否缓存内有动态方法 > 假如没有缓存,使用CreateParamInfoGenerator方法Emit IL创建AddParameter动态方法 > 创建完后保存在缓存内

接着重点来看CreateParamInfoGenerator方法内的底成逻辑跟「精美细节处理」,使用告终果反推代码方法,忽略「没使用的栏位」不生成对应IL代码,避免资源浪费状况。这也是前面缓存算法要去判断不一样SQL字串的缘由。

如下是我挑出的源码重点部分 :

internal static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals)
{
  //...略
  if (filterParams)
  {
    props = FilterParameters(props, identity.sql);
  }

  var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt;
  foreach (var prop in props)
  {
    //Emit IL动做
  }
  //...略
}


private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyInfo> parameters, string sql)
{
  var list = new List<PropertyInfo>(16);
  foreach (var p in parameters)
  {
    if (Regex.IsMatch(sql, @"[?@:]" + p.Name + @"([^\p{L}\p{N}_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant))
      list.Add(p);
  }
  return list;
}

接着查看IL来验证,查询代码以下

var result = connection.Query("select @Name name ", new { Name = "暐翰", Age = 26}).First();

CreateParamInfoGenerator AddParameter 动态方法IL代码以下 :

IL_0000: ldarg.1    
IL_0001: castclass  <>f__AnonymousType1`2[System.String,System.Int32]
IL_0006: stloc.0    
IL_0007: ldarg.0    
IL_0008: callvirt   System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand
IL_000d: dup        
IL_000e: ldarg.0    
IL_000f: callvirt   System.Data.IDbDataParameter CreateParameter()/System.Data.IDbCommand
IL_0014: dup        
IL_0015: ldstr      "Name"
IL_001a: callvirt   Void set_ParameterName(System.String)/System.Data.IDataParameter
IL_001f: dup        
IL_0020: ldc.i4.s   16
IL_0022: callvirt   Void set_DbType(System.Data.DbType)/System.Data.IDataParameter
IL_0027: dup        
IL_0028: ldc.i4.1   
IL_0029: callvirt   Void set_Direction(System.Data.ParameterDirection)/System.Data.IDataParameter
IL_002e: dup        
IL_002f: ldloc.0    
IL_0030: callvirt   System.String get_Name()/<>f__AnonymousType1`2[System.String,System.Int32]
IL_0035: dup        
IL_0036: brtrue.s   IL_0042
IL_0038: pop        
IL_0039: ldsfld     System.DBNull Value/System.DBNull
IL_003e: ldc.i4.0   
IL_003f: stloc.1    
IL_0040: br.s       IL_005a
IL_0042: dup        
IL_0043: callvirt   Int32 get_Length()/System.String
IL_0048: ldc.i4     4000
IL_004d: cgt        
IL_004f: brtrue.s   IL_0058
IL_0051: ldc.i4     4000
IL_0056: br.s       IL_0059
IL_0058: ldc.i4.m1  
IL_0059: stloc.1    
IL_005a: callvirt   Void set_Value(System.Object)/System.Data.IDataParameter
IL_005f: ldloc.1    
IL_0060: brfalse.s  IL_0069
IL_0062: dup        
IL_0063: ldloc.1    
IL_0064: callvirt   Void set_Size(Int32)/System.Data.IDbDataParameter
IL_0069: callvirt   Int32 Add(System.Object)/System.Collections.IList
IL_006e: pop        
IL_006f: pop        
IL_0070: ret

IL转成对应C#代码:

public class TestType
{
  public static void TestMeThod(IDataReader P_0, object P_1)
  {
    var anon = (<>f__AnonymousType1<string, int>)P_1;
    IDataParameterCollection parameters = ((IDbCommand)P_0).Parameters;
    IDbDataParameter dbDataParameter = ((IDbCommand)P_0).CreateParameter();
    dbDataParameter.ParameterName = "Name";
    dbDataParameter.DbType = DbType.String;
    dbDataParameter.Direction = ParameterDirection.Input;
    object obj = anon.Name;
    int num;
    if (obj == null)
    {
      obj = DBNull.Value;
      num = 0;
    }
    else
    {
      num = ((((string)obj).Length > 4000) ? (-1) : 4000);
    }
    dbDataParameter.Value = obj;
    if (num != 0)
    {
      dbDataParameter.Size = num;
    }
    parameters.Add(dbDataParameter);
  }
}

能够发现虽然传递Age参数,可是SQL字串没有用到,Dapper不会去生成该栏位的SetParameter动做IL。这个细节处理真的要给Dapper一个赞!

 
 

19. IN 多集合参数化底层原理

为什么ADO.NET不支援IN 参数化,Dapper支援 ?

原理

  1. 判断参数的属性是否为IEnumerable类别子类别
  2. 假如是,以该参数名称为主 + Parameter正则格式找寻SQL内的参数字串 (正则格式 : ([?@:]参数名)(?!\w)(\s+(?i)unknown(?-i))?)
  3. 将找到的字串以() + 多个属性名称+流水号方式替换
  4. 依照流水号顺序依序CreateParameter > SetValue

关键程式部分
https://ithelp.ithome.com.tw/upload/images/20190925/20105988ouMJ6GRB7F.png

如下用sys.objects来查SQL Server的表格跟视图当追踪例子 :

var result = cn.Query(@"select * from sys.objects where type_desc In @type_descs", new { type_descs = new[] { "USER_TABLE", "VIEW" } });

Dapper会将SQL字串改为如下方式执行

select * from sys.objects where type_desc In (@type_descs1,@type_descs2)
-- @type_descs1 = nvarchar(4000) - 'USER_TABLE'
-- @type_descs2 = nvarchar(4000) - 'VIEW'

查看Emit IL能够发现跟以前的参数化IL很不同,很是的简短

IL_0000: ldarg.1    
IL_0001: castclass  <>f__AnonymousType0`1[System.String[]]
IL_0006: stloc.0    
IL_0007: ldarg.0    
IL_0008: callvirt   System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand
IL_000d: ldarg.0    
IL_000e: ldstr      "type_descs"
IL_0013: ldloc.0    
IL_0014: callvirt   System.String[] get_type_descs()/<>f__AnonymousType0`1[System.String[]]
IL_0019: call       Void PackListParameters(System.Data.IDbCommand, System.String, System.Object)/Dapper.SqlMapper
IL_001e: pop        
IL_001f: ret

转成C#代码来看,会很惊讶地发现:「这段根本不须要使用Emit IL简直画蛇添足」

public static void TestMeThod(IDbCommand P_0, object P_1)
    {
        var anon = (<>f__AnonymousType0<string[]>)P_1;
        IDataParameterCollection parameter = P_0.Parameters;
        SqlMapper.PackListParameters(P_0, "type_descs", anon.type_descs);
    }

没错,是画蛇添足,甚至IDataParameterCollection parameter = P_0.Parameters;这段代码根本不会用到。

Dapper这边作法是有缘由的,由于要能跟非集合参数配合使用,像是前面例子加上找出订单Orders名称的资料逻辑

var result = cn.Query(@"select * from sys.objects where type_desc In @type_descs and name like @name"
    , new { type_descs = new[] { "USER_TABLE", "VIEW" }, @name = "order%" });

对应生成的IL转换C#代码就会是如下代码,达到能搭配使用目的 :

public static void TestMeThod(IDbCommand P_0, object P_1)
    {
        <>f__AnonymousType0<string[], string> val = P_1;
        IDataParameterCollection parameters = P_0.Parameters;
        SqlMapper.PackListParameters(P_0, "type_descs", val.get_type_descs());
        IDbDataParameter dbDataParameter = P_0.CreateParameter();
        dbDataParameter.ParameterName = "name";
        dbDataParameter.DbType = DbType.String;
        dbDataParameter.Direction = ParameterDirection.Input;
        object obj = val.get_name();
        int num;
        if (obj == null)
        {
            obj = DBNull.Value;
            num = 0;
        }
        else
        {
            num = ((((string)obj).Length > 4000) ? (-1) : 4000);
        }
        dbDataParameter.Value = obj;
        if (num != 0)
        {
            dbDataParameter.Size = num;
        }
        parameters.Add(dbDataParameter);
    }

另外为什么Dapper这边Emit IL会直接呼叫工具方法PackListParameters,是由于IN的参数化数量是不固定,因此不能由固定结果反推程式码方式动态生成方法。

该方法里面包含的主要逻辑:

  1. 判断集合参数的类型是哪种 (假如是字串预设使用4000大小)
  2. 正则判断SQL参数以流水号参数字串取代
  3. DbCommand的Paramter的建立

https://ithelp.ithome.com.tw/upload/images/20190925/20105988KgYZmlciZJ.png
SQL参数字串的取代逻辑也写在这边,如图片
https://ithelp.ithome.com.tw/upload/images/20190925/20105988Rhner7LZPA.png

 
 

20.DynamicParameter 底层原理、自订实做

这边用个例子带读者了解DynamicParameter原理,举例如今有一段代码以下 :

using (var cn = Connection)
{
    var paramter = new { Name = "John", Age = 25 };
    var result = cn.Query("select @Name Name,@Age Age", paramter).First();
}

前面已经知道String型态Dapper会自动将转成数据库Nvarchar而且长度为4000的参数,数据库实际执行的SQL以下 :

exec sp_executesql N'select @Name Name,@Age Age',N'@Name nvarchar(4000),@Age int',@Name=N'John',@Age=25

这是一个方便快速开发的贴心设计,但假如遇到栏位是varchar型态的状况,有可能会由于隐性转型致使索引失效,致使查询效率变低。

这时解决方式可使用Dapper DynamicParamter指定数据库型态跟大小,达到优化效能目的

using (var cn = Connection)
{
    var paramters = new DynamicParameters();
    paramters.Add("Name","John",DbType.AnsiString,size:4);
    paramters.Add("Age",25,DbType.Int32);
    var result = cn.Query("select @Name Name,@Age Age", paramters).First();
}

接着往底层来看如何实现,首先关注GetCacheInfo方法,能够看到DynamicParameters创建动态方法方式代码很简单,就只是呼叫AddParameters方法

Action<IDbCommand, object> reader;
if (exampleParameters is IDynamicParameters)
{
    reader = (cmd, obj) => ((IDynamicParameters)obj).AddParameters(cmd, identity);
}

代码能够这么简单的缘由,是Dapper在这边特别使用「依赖于介面」设计,增长程式的弹性,让使用者能够客制本身想要的实做逻辑。这点下面会讲解,首先来看Dapper预设的实做类别DynamicParametersAddParameters方法的实做逻辑

public class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper.IParameterLookup, SqlMapper.IParameterCallbacks
{
    protected void AddParameters(IDbCommand command, SqlMapper.Identity identity)
    {
        var literals = SqlMapper.GetLiteralTokens(identity.sql);

        foreach (var param in parameters.Values)
        {
            if (param.CameFromTemplate) continue;

            var dbType = param.DbType;
            var val = param.Value;
            string name = Clean(param.Name);
            var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter;

            SqlMapper.ITypeHandler handler = null;
            if (dbType == null && val != null && !isCustomQueryParameter)
            {
#pragma warning disable 618
                dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler);
#pragma warning disable 618
            }
            if (isCustomQueryParameter)
            {
                ((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name);
            }
            else if (dbType == EnumerableMultiParameter)
            {
#pragma warning disable 612, 618
                SqlMapper.PackListParameters(command, name, val);
#pragma warning restore 612, 618
            }
            else
            {
                bool add = !command.Parameters.Contains(name);
                IDbDataParameter p;
                if (add)
                {
                    p = command.CreateParameter();
                    p.ParameterName = name;
                }
                else
                {
                    p = (IDbDataParameter)command.Parameters[name];
                }

                p.Direction = param.ParameterDirection;
                if (handler == null)
                {
#pragma warning disable 0618
                    p.Value = SqlMapper.SanitizeParameterValue(val);
#pragma warning restore 0618
                    if (dbType != null && p.DbType != dbType)
                    {
                        p.DbType = dbType.Value;
                    }
                    var s = val as string;
                    if (s?.Length <= DbString.DefaultLength)
                    {
                        p.Size = DbString.DefaultLength;
                    }
                    if (param.Size != null) p.Size = param.Size.Value;
                    if (param.Precision != null) p.Precision = param.Precision.Value;
                    if (param.Scale != null) p.Scale = param.Scale.Value;
                }
                else
                {
                    if (dbType != null) p.DbType = dbType.Value;
                    if (param.Size != null) p.Size = param.Size.Value;
                    if (param.Precision != null) p.Precision = param.Precision.Value;
                    if (param.Scale != null) p.Scale = param.Scale.Value;
                    handler.SetValue(p, val ?? DBNull.Value);
                }

                if (add)
                {
                    command.Parameters.Add(p);
                }
                param.AttachedParam = p;
            }
        }

        // note: most non-priveleged implementations would use: this.ReplaceLiterals(command);
        if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals);
    }
}

能够发现Dapper在AddParameters为了方便性跟兼容其余功能,像是Literal Replacement、EnumerableMultiParameter功能,作了许多判断跟动做,因此代码量会比之前使用ADO.NET版本多,因此效率也会比较慢。

假若有效率苛求的需求,能够本身实做想要的逻辑,由于Dapper此段特别设计成「依赖于介面」,只须要实做IDynamicParameters介面就能够。

如下是我作的一个Demo,可使用ADO.NET SqlParameter创建参数跟Dapper配合

public class CustomPraameters : SqlMapper.IDynamicParameters
{
  private SqlParameter[] parameters;
  public void Add(params SqlParameter[] mParameters)
  {
    parameters = mParameters;
  }

  void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity)
  {
    if (parameters != null && parameters.Length > 0)
      foreach (var p in parameters)
        command.Parameters.Add(p);
  }
}

https://ithelp.ithome.com.tw/upload/images/20191005/20105988qzCAsa5KZu.png

 
 

21. 单次、屡次 Execute 底层原理

查询、Mapping、参数讲解完后,接着讲解在增、删、改状况Dapper咱们会使用Execute方法,其中Execute Dapper分为单次执行、屡次执行

单次Execute

以单次执行来讲Dapper Execute底层是ADO.NET的ExecuteNonQuery的封装,封装目的为了跟Dapper的Parameter、缓存功能搭配使用,代码逻辑简洁明了这边就不作多说明,如图片
20191002144453.png

「屡次」Execute

这是Dapper一个特点功能,它简化了集合操做Execute之间的操做,简化了代码,只须要 : connection.Execute("sql",集合参数);

至于为什么能够这么方便,如下是底层的逻辑 :

  1. 确认是否为集合参数
    20191002150155.png
  2. 创建一个共同DbCommand提供foreach迭代使用,避免重复创建浪费资源
    20191002151237.png
  3. 假如是集合参数,创建Emit IL动态方法,并放在缓存内利用
    20191002150349.png
  4. 动态方法逻辑是CreateParameter > 对Parameter赋值 > 使用Parameters.Add添加新建的参数,如下是Emit IL转成的C#代码 :
public static void ParamReader(IDbCommand P_0, object P_1)
  {
    var anon = (<>f__AnonymousType0<int>)P_1;
    IDataParameterCollection parameters = P_0.Parameters;
    IDbDataParameter dbDataParameter = P_0.CreateParameter();
    dbDataParameter.ParameterName = "V";
    dbDataParameter.DbType = DbType.Int32;
    dbDataParameter.Direction = ParameterDirection.Input;
    dbDataParameter.Value = anon.V;
    parameters.Add(dbDataParameter);
  }
  1. foreach该集合参数 > 除了第一次外,每次迭代清空DbCommand的Parameters > 从新呼叫同一个动态方法添加Parameter > 送出SQL查询

实做方式简洁明了,而且细节考虑共用资源避免浪费(e.g共用同一个DbCommand、Func),但遇到大量执行追求效率需求状况,须要特别注意此方法每跑一次对数据库送出一次reqesut,效率会被网路传输拖慢,因此这功能被称为「屡次执行」而不是「批量执行」的主要缘由。

举例,简单Execute插入十笔资料,查看SQL Profiler能够看到系统接到10次Reqeust:

using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=Northwind;"))
{
    cn.Open();
    using (var tx = cn.BeginTransaction())
    {
        cn.Execute("create table #T (V int);", transaction: tx);
        cn.Execute("insert into #T (V) values (@V)", Enumerable.Range(1, 10).Select(val => new { V = val }).ToArray() , transaction:tx);

        var result = cn.Query("select * from #T", transaction: tx);
        Console.WriteLine(result);
    }
}

20191002151658.png

 
 

22. ExecuteScalar应用

ExecuteScalar由于其只能读取第一组结果、第一笔列、第一笔资料特性,是一个常被遗忘的功能,但它在特定需求下仍是能派上用场,底下用「查询资料是否存在」例子来作说明。

首先,Entity Framwork如何高效率判断资料是否存在?

假若有EF经验的读者会答使用Any而不是Count() > 1

使用Count系统会帮转换SQL为 :

SELECT COUNT(*) AS [value] FROM [表格] AS [t0]

SQL Count 是一个汇总函数,会迭代符合条件的资料行判断每列该资料是否为null,并返回其行数。

而Any语法转换SQL使用EXISTS,它只在意是否有没有资料,表明不用检查到每列,只须要其中一笔有资料就有结果,因此效率快。

SELECT 
    (CASE 
        WHEN EXISTS(
            SELECT NULL AS [EMPTY]
            FROM [表格] AS [t0]
            ) THEN 1
        ELSE 0
     END) AS [value]

Dapper如何作到一样效果?

SQL Server可使用SQL格式select top 1 1 from [表格] where 条件 搭配 ExecuteScalar 方法,接着在作一个扩充方法,以下 :

public static class DemoExtension
{
  public static bool Any(this IDbConnection cn,string sql,object paramter = null)
  {
    return cn.ExecuteScalar<bool>(sql,paramter);
  }
}

效果图 :
20191003043825.png

使用如此简单缘由,是利用Dapper ExecuteScalar会去呼叫ExecuteScalarImpl其底层Parse逻辑

private static T ExecuteScalarImpl<T>(IDbConnection cnn, ref CommandDefinition command)
{
    //..略
    object result;
    //..略
    result = cmd.ExecuteScalar();
    //..略
    return Parse<T>(result);
}

private static T Parse<T>(object value)
{
    if (value == null || value is DBNull) return default(T);
    if (value is T) return (T)value;
    var type = typeof(T);
    //..略
    return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
}

使用 Convert.ChangeType 转成 bool : 「0=false,非0=true」 特性,让系统能够简单转型为bool值。

注意

不要QueryFirstOrDefault代替,由于它须要在SQL额外作Null的判断,不然会出现「NullReferenceException」。
20191003043931.png

这缘由是二者Parse实做方式不同,QueryFirstOrDefault判断结果为null时直接强转型
20191003043941.png

而ExecuteScalar的Parce实做多了为空时使用default值的判断
20191003043953.png

 
 

23.总结

Dapper系列到这边,重要底层原理差很少都讲完了,这系列总共花了笔者连续25天的时间,除了想帮助读者外,最大的收获就是我本身在这期间更了解Dapper底层原理,而且学习Dapper精心的细节、框架处理。

另外想提Dapper做者之一Marc Gravell,真的很是热心,在写文章的期间有几个概念疑问,发issue询问,他都会热心、详细的回覆。而且也发现他对代码的品质要求之高,举例 : 在S.O发问,遇到他在底下留言 : 「他对目前Dapper IL的架构实际上是不满意的,甚至以为粗糙,想搭配protobuf-net技术打掉重写」 (谜之声 : 真使人敬佩 )

连结 : c# - How to remove the last few segments of Emit IL at runtime - Stack Overflow
https://ithelp.ithome.com.tw/upload/images/20190925/201059884hfaioQATW.png

最后笔者想说 :
写这篇的初衷,是但愿本系列能够帮助到读者

  1. 了解底层逻辑,知其因此然,避免写出吃掉效能的怪兽,更进一步完整的利用Dapper优势开发专案
  2. 能够轻松面对Dapper的面试,比起通常使用Dapper工程师回答出更深层的概念
  3. 从最简单Reflection到经常使用Expression到最细节Emit从头创建Mapping方法,带读者渐进式了解Dapper底层强型别Mapping逻辑
  4. 了解动态创建方法的重要概念「结果反推程式码」
  5. 有基本IL能力,能够利用IL反推C#代码方式看懂其余专案的底层Emit逻辑
  6. 了解Dapper由于缓存的算法逻辑,因此不能使用错误字串拼接SQL

感谢你们阅读到最后,假如喜欢本系列,欢迎留言、交流 :)

相关文章
相关标签/搜索