通過業界前輩、StackOverflow多年推廣,「Dapper搭配Entity Framework」成爲一種功能強大的組合,它知足「安全、方便、高效、好維護」
需求。html
但目前中文網路文章,雖然有不少關於Dapper的文章但都停留在如何使用,沒人系統性解說底層原理。因此有了此篇「深刻Dapper源碼」想帶你們進入Dapper底層,瞭解Dapper的精美細節設計、高效原理,並學起來
實際應用在工做當中。git
在前期開發階段由於表格結構還在調整階段
,或是不值得額外宣告類別輕量需求,使用Dapper dynamic Query能夠節省下來回修改class屬性的時間。當表格穩定下來後使用POCO生成器快速生成Class轉成強型別
維護。github
追溯Query
方法源碼能夠發現兩個重點面試
DapperRow
再隱性轉型爲dynamic。IDynamicMetaObjectProvider
而且實做對應方法。此段邏輯我這邊作一個簡化版本的Dapper dynamic Query讓讀者瞭解轉換邏輯 :算法
dynamic
類別變量,實體類別是ExpandoObject
IDictionary<string, object>
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; } }
有了前面簡單ExpandoObject Dynamic Query例子的概念後,接着進到底層來了解Dapper如何細節處理,爲什麼要自訂義DynamicMetaObjectProvider。sql
假設使用下面代碼數據庫
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呼叫、緩存重複利用。
c#
此段Func邏輯 :緩存
DapperTable雖然是方法內的局部變量,可是被生成的Func引用,因此不會被GC
一直保存在內存內重複利用。
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; }
public DapperRow(DapperTable table, object[] values) { this.table = table ?? throw new ArgumentNullException(nameof(table)); this.values = values ?? throw new ArgumentNullException(nameof(values)); }
private sealed partial class DapperRow : System.Dynamic.IDynamicMetaObjectProvider { DynamicMetaObject GetMetaObject(Expression parameter) { return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); } }
DapperRowMetaObject主要功能是定義行爲,藉由override BindSetMember、BindGetMember
方法,Dapper定義了Get、Set的行爲分別使用IDictionary<string, object> - GetItem方法
跟DapperRow - SetValue方法
最後Dapper利用DataReader的欄位順序性
,先利用欄位名稱取得Index,再利用Index跟Values取得值
能夠思考一個問題 : 在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> }
接下來是Dapper關鍵功能 Strongly Typed Mapping
,由於難度高,這邊會切分紅多篇來解說。
第一篇先以ADO.NET DataReader GetItem By Index跟Dapper Strongly Typed Query對比,查看二者IL的差別,瞭解Dapper Query Mapping的主要邏輯。
有了邏輯後,如何實做,我這邊依序用三個技術 :Reflection、Expression、Emit
從頭實做三個版本Query方法來讓讀者漸進式瞭解。
首先使用如下代碼來追蹤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技術。
要了解這段IL邏輯,個人方式 :「不該該直接進到細節,而是先查看完整生成的IL」
,至於如何查看,這邊須要先準備 il-visualizer 開源工具,它能夠在Runtime查看DynamicMethod生成的IL。
它預設支持vs 201五、2017,假如跟我同樣使用vs2019的讀者,須要注意
%USERPROFILE%\Documents\Visual Studio 2019
路徑下面.netstandard2.0
專案,須要創建netstandard2.0
並解壓縮到該資料夾最後重開visaul studio並debug運行,進到GetTypeDeserializerImpl方法,對DynamicMethod點擊放大鏡 > 選擇IL visualizer > 查看Runtime
生成的IL代碼
能夠得出如下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
的緣由之一。
在前面ADO.NET Mapping例子能夠發現嚴重問題「沒辦法多類別共用方法,每新增一個類別就須要重寫代碼」
。要解決這個問題,能夠寫一個共用方法在Runtime時期針對不一樣的類別作不一樣的邏輯處理。
實做方式作主要有三種Reflection、Expression、Emit,這邊首先介紹最簡單方式:「Reflection」,我這邊會使用反射方式從零模擬Query寫代碼,讓讀者初步瞭解動態處理概念。(假若有經驗的讀者能夠跳過本篇)
邏輯 :
泛型的條件約束new()
達到動態創建物件屬性字串名稱當Key
,可使用Reflection取得動態類別的屬性名稱,在藉由DataReader this[string parameter]
取得數據庫資料最後獲得如下代碼 :
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版本優勢是代碼簡單
,但它有如下問題
「查多少用多少,不浪費」
(這段以後講解)。「查表法 + 動態創建方法」
以空間換取時間。GetOrdinal
方法,能夠查看MSDN官方解釋,效率比Index取值差
。
接着使用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就是使用此概念來動態創建方法。
舉例 : 假設有一段代碼以下,咱們能夠從結果得出
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在緩存的算法特別針對此優化。
有了前面的邏輯,就着使用Expression實做動態創建方法。
除了有能力動態創建方法,相比Emit有如下優勢 :
可讀性好
,可用熟悉的關鍵字,像是變量Variable對應Expression.Variable、創建物件New對應Expression.New方便Runtime Debug
,能夠在Debug模式下看到Expression對應邏輯代碼因此特別適合介紹動態方法創建,但Expression相比Emit沒法做一些細節操做,這點會在後面Emit講解到。
邏輯 :
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(); } } }
查詢效果圖 :
最後查看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 } }
有了前面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(); }
可是對已經寫好的專案來講就不是這樣流程了,開發者不必定會好心的告訴你當初設計的邏輯,因此接着討論此問題。
個人解決方式是 : 「既然只有Runtime才能知道IL,那麼將IL保存成靜態檔案再反編譯查看」
這邊可使用MethodBuild + Save
方法將IL保存成靜態exe檔案 > 反編譯查看
,但須要特別注意
region if 指定版本
來作區分,不然不能使用,如圖片代碼以下 :
//使用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; } }
有了C#代碼後再來了解Emit邏輯會快不少,接着就能夠進到Emit版本Query實做部分。
如下代碼是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 if/else須要使用Label定位,告知編譯器條件爲true/false時要跳到哪一個位子,舉例 : 「boolean轉整數」,假設要簡單將Boolean轉換成Int,C#代碼能夠用「若是是True返回1不然返回0」邏輯來寫:
public static int BoolToInt(bool input) => input ? 1 : 0;
當轉成Emit寫法的時候,須要如下邏輯 :
(注意,這時候Label位子還沒肯定)
符合條件
要運行區塊的前一行
,使用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版本
優勢 :
缺點 :
接着來看Dapper做者的建議,如今通常專案當中沒有必要使用Emit,使用Expression + Func/Action已經能夠解決大部分動態方法的需求,尤爲是Expression支援Block等方法狀況。連結 c# - What's faster: expression trees or manually emitting IL
話雖如此,但有一些厲害的開源專案就是使用Emit管理細節,若是想看懂它們,就須要基礎的Emit IL概念
。
前面介紹到動態使用 Emit IL 創建 ADO.NET Mapping 方法,但單就這功能沒法讓 Dapper 被稱爲輕量ORM效率之王。
由於動態創建方法是須要成本、並耗費時間
的動做,單純使用反而會拖慢速度。但當配合 Cache 後就不同,將創建好的方法保存在 Cache 內,能夠用『空間換取時間』
概念加快查詢的效率,也就是俗稱查表法
。
接着追蹤Dapper源碼,此次須要特別關注的是QueryImpl方法下的Identity、GetCacheInfo
Identity主要封裝各緩存的比較Key屬性 :
接着搭配GetCacheInfo方法內Dapper使用的緩存類別ConcurrentDictionary<Identity, CacheInfo>
,使用TryGetValue
方法時會去先比對HashCode接着比對Equals特性,如圖片源碼。
將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) { //..略 } }
效果圖 :
瞭解實做邏輯後,接着延伸一個Dapper使用的重要觀念,SQL字串
爲緩存重要Key值之一,假如不一樣的SQL字串,Dapper會爲此創建新的動態方法、緩存,因此使用不當狀況下就算使用StringBuilder也會形成效率慢、內存泄漏問題
。
至於爲什麼要以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(); } } }
要避免此問題,只須要保持一個原則重複利用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(); } } }
假如遇到必要拼接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(); } }
首先追蹤源碼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問題。
接着講解Dapper Multi 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; } }
在初期常變更表格結構或是一次性功能不想宣告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;")) { //略.. }
感受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(); } } } }
遇到想要客制某些屬性Mapping邏輯時,在Dapper可使用TypeHandler
使用方式 :
SqlMapper.TypeHandler
泛型
,e.g : JsonTypeHandler<客制類別> : SqlMapper.TypeHandler<客制類別>
查詢
的邏輯使用override實做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; } }
效果圖 :
接着追蹤TypeHandler源碼邏輯,須要分兩個部份來追蹤 : SetValue,Parse
if (handler != null) { il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache<int>.SetValue))); // stack is now [parameters] [[parameters]] [parameter] }
最後查看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,這樣有如下優勢 :
避免重複創建物件
提高效率
主要邏輯是在GenerateDeserializerFromMap方法Emit創建動態Mapping方法時,假如判斷TypeHandler緩存有資料,以Parse方法取代本來的Set屬性動做。
查看動態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; } }
這篇將帶讀者瞭解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 |
首先能夠看到每一個方法都使用CommandBehavior.SequentialAccess
,該標籤主要功能 使DataReader順序讀取行和列,行和列不緩衝,讀取一列後,它會從內存中刪除。
,有如下優勢 :
避免二進制大資源一次性讀取到內存
,尤爲是Blob或是Clob會配合GetBytes 或 GetChars 方法限制緩衝區大小,微軟官方也特別標註注意 :加快查詢效率
但它卻不是
DataReader的預設行爲,系統預設是CommandBehavior.Default
CommandBehavior.Default有着如下特性 :
多個
結果集(Multi Result)這兩個特性跟生產環境狀況差滿多,畢竟大多時刻是只須要一組結果集配合有限的內存
,因此除了SequentialAccess外Dapper還特別在大多方法使用了CommandBehavior.SingleResult
,知足只需一組結果就好避免浪費資源。
這段還有一段細節的處理,查看源碼能夠發現除了標記SingleResult外,Dapper還特別加上一段代碼在結尾while (reader.NextResult()){}
,而不是直接Return(如圖片)
早些前我有特別發Issue(連結#1210)詢問過做者,這邊是回答 : 主要避免忽略錯誤,像是在DataReader提前關閉狀況
有時候咱們會遇到select top 1
知道只會讀取一行資料的狀況,這時候可使用QueryFirst
。它使用CommandBehavior.SingleRow
能夠避免浪費資源只讀取一行資料。
另外能夠發現此段除了while (reader.NextResult()){}
外還有while (reader.Read()) {}
,一樣是避免忽略錯誤,這是一些公司自行土炮ORM會忽略的地方。
二者差異在QuerySingle沒有使用CommandBehavior.SingleRow,至於爲什麼沒有使用,是由於須要有多行資料才能判斷是否不符合條件並拋出Exception告知使用者
。
這段有一個特別好玩小技巧能夠學,錯誤處理直接沿用對應LINQ的Exception,舉例:超過一行資料錯誤,使用new int[2].Single()
,這樣不用另外維護Exceptiono類別,還能夠擁有i18N多國語言化。
接着進到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一個贊!
IN 參數化
,Dapper支援 ?原理
([?@:]參數名)(?!\w)(\s+(?i)unknown(?-i))?
)()
+ 多個屬性名稱+流水號
方式替換關鍵程式部分
如下用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的參數化數量是不固定
,因此不能由固定結果反推程式碼
方式動態生成方法。
該方法裏面包含的主要邏輯:
SQL參數字串的取代邏輯也寫在這邊,如圖片
這邊用個例子帶讀者瞭解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預設的實做類別DynamicParameters
中AddParameters
方法的實做邏輯
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); } }
查詢、Mapping、參數講解完後,接着講解在增、刪、改
狀況Dapper咱們會使用Execute方法,其中Execute Dapper分爲單次執行、屢次執行
。
以單次執行來講Dapper Execute底層是ADO.NET的ExecuteNonQuery的封裝,封裝目的爲了跟Dapper的Parameter、緩存
功能搭配使用,代碼邏輯簡潔明瞭這邊就不作多說明,如圖片
這是Dapper一個特點功能,它簡化了集合操做Execute之間的操做,簡化了代碼,只須要 : connection.Execute("sql",集合參數);
。
至於爲什麼能夠這麼方便,如下是底層的邏輯 :
一個共同DbCommand
提供foreach迭代使用,避免重複創建浪費資源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); }
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); } }
ExecuteScalar由於其只能讀取第一組結果、第一筆列、第一筆資料
特性,是一個常被遺忘的功能,但它在特定需求下仍是能派上用場,底下用「查詢資料是否存在」例子來作說明。
假若有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]
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); } }
效果圖 :
使用如此簡單緣由,是利用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」。
這緣由是二者Parse實做方式不同,QueryFirstOrDefault判斷結果爲null時直接強轉型
而ExecuteScalar的Parce實做多了爲空時使用default值
的判斷
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
最後筆者想說 :
寫這篇的初衷,是但願本系列能夠幫助到讀者
漸進式
瞭解Dapper底層強型別Mapping邏輯「結果反推程式碼」
不能使用錯誤字串拼接SQL
感謝你們閱讀到最後,假如喜歡本系列,歡迎留言、交流 :)