用事實說話,成熟的ORM效能不是瓶頸,靈活性不是問題:EF5.0、PDF.NET5.0、Dapper原理分析與測試手記
一、ORM的“三國志”
1,PDF.NET誕生歷程
記得我很早以前(大概05年以前),剛聽到ORM這個詞的時候,就聽到有人在說ORM效能不高,要求效能的地方都是直接SQL的,後來談論ORM的人越來越多的時候,我也去關注了下,偶然間發現,尼瑪,一個文章表的實體類,居然查詢的時候把Content(內容)欄位也查詢出來了,這要是我查詢個文章列表,這些內容欄位不僅多餘,而且嚴重影響效能,為啥不能只查詢我需要的欄位到ORM?自此對ORM沒有好感,潛心研究SQL去了,將SQL封裝到一個XML檔案程式再來呼叫,還可以在執行時修改,別提多爽了,ORM,一邊去吧:)
到了06年,隨著這種寫SQL的方式,我發現一個專案裡面CRUD的SQL實在是太多了,特別是分頁,也得手寫SQL去處理,為了高效率,分頁還要3種方式,第一頁直接用Top,最後一頁也用Top倒序處理,中間就得使用雙OrderBy處理了。這些SQL寫多了越寫越煩,於是再度去圍觀ORM,發現它的確大大減輕了我寫SQL的負擔,除了那個令我心煩的Content內容欄位也被查詢出來的問題,不過我也學會了,單獨建立一個實體類,影射文章表的時候,不對映Content內容欄位即可。很快發現,煩心的不止這個Content內容欄位,如果要做到SQL那麼靈活,要讓系統更加高效,有很多地方實體類都不需要完整對映一個表的,一個表被影射出3-4個實體類是常見的事情,這讓系統的實體類數量迅速膨脹... 看來我不能忍受ORM的這個毛病了,必須為ORM搞一個查詢的API,讓ORM可以查詢指定的屬性,而不是從資料庫查詢全部的屬性資料出來,這就是OQL的雛形:
User u=new User(); u.Age=20; OQL oql=new OQL(u); oql.Select(u.UserID,u.Name,u.Sex).Where(u.Age); List<User> list=EntityQuery<User>.QueryList(q);
上面是查詢年齡等於20的使用者的ID,Name,Sex 資訊,當然User 實體類還有其它屬性,當前只需要這幾個屬性。
當時這個ORM查詢API--OQL很簡單,只能處理相等條件的查詢,但是能夠只選取實體類的部分屬性,已經很好了,複雜點的查詢,結合在XML中寫SQL語句的方式解決,其它一些地方,通過資料控制元件,直接生成SQL語句去執行了,比如用資料控制元件來更新表單資料到資料庫。
小結一下我做CRUD的歷史,首先是對寫SQL樂此不彼,還發明瞭在XML檔案中配置SQL然後對映到程式的功能:SQL-MAP,然後覺得這樣寫SQL儘管方便管理編寫查詢且可以自動生成DAL程式碼,但是專案裡面大量的SQL還是導致工作量很大,於是拿起ORM併發明瞭查詢部分實體類屬性的查詢API:OQL;最後,覺得有些地方用ORM還是麻煩,比如處理一個表單的CRUD,如果用ORM也得收集或者填充資料到實體類上,還不如直接發出SQL,於是又有了“資料控制元件”。
這樣,按照出現的順序,在2006年11月,一個具有SQL-MAP、ORM、Data Control功能的資料開發框架:PDF.NET Ver 1.0 誕生了!
2,Linq2Sql&EF:
2008年,隨著.NET 3.5和VS2008釋出,MS的官方ORM框架Linq2Sql也一同釋出了,它採用Linq語法來查詢資料庫,也就是說Linq是MS的ORM查詢API。由於Linq語法跟SQL語法有較大的區別,特別是Linq版本的左、又連線查詢語法,跟SQL的Join連線查詢,差異巨大,因此,學習Linq需要一定的成本。但是,LINQ to SQL是一個不再更新的技術。其有很多不足之處,如,不能靈活的定義物件模型與資料表之間的對映、無法擴充套件提供程式只能支援SQL Server等。 MS在同年,推出了Entity Framework,大家習慣的簡稱它為EF,它可以支援更多的資料庫。於是在2008年12月,我原來所在公司的專案經理急切的準備去嘗試它,用EF去開發一個用Oracle的系統。到了2009年8月,坊間已經到處流傳,Linq2Sql將死,EF是未來之星,我們當時有一個客戶端專案,準備用EF來訪問SQLite。當時我任該專案的專案經理,由於同事都不怎麼會Linq,更別提EF了,於是部分模組用傳統的DataSet,部分用了EF for SQLite。結果專案做完,兩部分模組進行對比,發現用EF的模組,訪問速度非常的慢,查詢複雜一下直接要5秒以上才出結果,對這些複雜的查詢不得不直接用SQL去重寫,而自此以後,我們公司再也沒有人在專案中使用EF了,包括我也對EF比較失望,於是重新撿起我的PDF.NET,並在公司後來的諸多專案中大量推廣使用。
最近一兩年,坊間流行DDD開發,提倡Code First了,談論EF的人越來越多了,畢竟EF的查詢API--LINQ,是.NET的親生兒子,大家都愛上了它,那麼愛EF也是自然的。在EF 5.0的時候,它已經完全支援Code First了,有人說現在的EF速度很快了,而我對此,還是半信半疑,全力發展PDF.NET,現在它也支援Code First 開發模式了。
3,微型ORM崛起
也是最近兩年,談論微型ORM的人也越來越多了,它們主打“靈活”、“高效能”兩張牌,查詢不用Linq,而是直接使用SQL或者變體的SQL語句,將結果直接對映成POCO實體類。由於它們大都採用了Emit的方式根據DataReader動態生成實體類的對映程式碼,所以這類微型ORM框架的速度接近手寫映射了。這類框架的代表就是Dapper、PetaPOCO.
二、一決高下
1,ORM沒有DataSet快?
這個問題由來已久,自ORM誕生那一天起就有不少人在疑問,甚至有人說,複雜查詢,就不該用ORM(見《為什麼不推崇複雜的ORM http://www.cnblogs.com/wushilonng/p/3349512.html》,不僅查詢語法不靈活,效能也底下。對此問題,我認為不管是Linq,還是OQL,或者是別的什麼ORM的查詢API,要做到SQL那麼靈活的確不可能,所以Hibernate還有HQL,EF還有ESQL,基於字串的實體查詢語句,但我覺得既然都字串了還不如直接SQL來的好;而對於複雜查詢效率低的問題,這個跟ORM沒有太大關係,複雜查詢哪怕用SQL寫,DB執行起來也低的,ORM只不過自動生成SQL讓DB去執行而已,問題可能出在某些ORM框架輸出的SQL並不是開發人員預期的,也很難對它輸出的SQL語句進行優化,從而導致效率較低,但這種情況並不多見,不是所有的查詢ORM輸出的SQL都很爛,某些SQL還是優化的很好的,只不過優化權不再開發人員手中。另外,有的ORM語言可以做到查詢透明化的,即按照你用ORM的預期去生成對應的SQL,不會花蛇添足,PDF.NET的ORM查詢語言OQL就是這樣的。
那麼,對於一般的查詢,ORM有沒有DataSet快?
很多開發人員自己造的ORM輪子可能會有這個問題,依靠反射,將DataReader的資料讀取到實體類上,這種方式效率很低,肯定比DataSet慢,現在,大部分成熟的ORM框架,對此都改進了,通常的做法是使用委託、表示式樹、Emit來解決這個問題,Emit效率最高,表示式樹的解析會消耗不少資源,早期的EF,不知道是不是這個問題,也是慢得出奇;而採用委託方式,有所改進,但效率不是很高,如果結合快取,那麼效率提升就較為明顯了。
由於大部分ORM框架都是採用DataReader來讀取資料的,而DataSet依賴於DataAdapter,本身DataReader就是比DataSet快的,所以只要解決了DataReader閱讀器賦值給實體類的效率問題,那麼這樣的ORM就有可能比DataSet要快的,況且,弱型別的DataSet,在查詢的時候會有2次查詢,第一次是查詢架構,第二次才是載入資料,所以效率比較慢,因此,採用強型別的DataSet,能夠改善這個問題,但要使用自定義的Sql查詢來填充強型別的DataSet的話,又非常慢,比DataSet慢了3倍多。
2,ORM的三個火槍手
今天,我們就用3個框架,採用3種不同的方式實現的ORM ,來比較下看誰的效率最高。在比賽前,我們先分別看看3種ORM的實現方式。
2.1,委託+快取
我們首先得知道,怎麼對一個屬性進行讀寫,可以通過反射實現,如下面的程式碼:
PropertyInfo.GetValue(source,null); PropertyInfo.SetValue(target,Value ,null);
PropertyInfo 是物件的屬性資訊物件,可以通過反射拿到物件的每個屬性的屬性資訊物件,我們可以給它定義一個委託來分別對應屬性的讀寫:
public Func<object, object[], object> Getter { get; private set; } public Action<object, object, object[]> Setter { get; private set; }
我們將Getter委託繫結到PropertyInfo.GetValue 方法上,將Setter委託繫結到PropertyInfo.SetValue 方法上,那麼在使用的時候可以象下面這個樣子:
CastProperty cp = mProperties[i]; if (cp.SourceProperty.Getter != null) { object Value = cp.SourceProperty.Getter(source, null); //PropertyInfo.GetValue(source,null); if (cp.TargetProperty.Setter != null) cp.TargetProperty.Setter(target, Value, null);// PropertyInfo.SetValue(target,Value ,null); }
這段程式碼來自我以前的文章《使用反射+快取+委託,實現一個不同物件之間同名同類型屬性值的快速拷貝》,型別的所有屬性都已經事先快取到了mProperties 陣列中,這樣可以在一定程度上改善反射的缺陷,加快屬性讀寫的速度。
但是,上面的方式不是最好的,原因就在於PropertyInfo.GetValue、PropertyInfo.SetValue 很慢,因為它的引數和返回值都是 object 型別,會有型別檢查和型別轉換,因此,採用泛型委託才是正道。
private MyFunc<T, P> GetValueDelegate; private MyAction<T, P> SetValueDelegate; public PropertyAccessor(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName); if (propertyInfo != null) { GetValueDelegate = (MyFunc<T, P>)Delegate.CreateDelegate(typeof(MyFunc<T, P>), propertyInfo.GetGetMethod()); SetValueDelegate = (MyAction<T, P>)Delegate.CreateDelegate(typeof(MyAction<T, P>), propertyInfo.GetSetMethod()); } }
上面的程式碼定義了GetValueDelegate 委託,指向屬性的 GetGetMethod()方法,定義SetValueDelegate,指向屬性的GetSetMethod()方法。有了這兩個泛型委託,我們訪問一個屬性,就類似於下面這個樣子了:
string GetUserNameValue<User>(User instance) { return GetValueDelegate<User,string>(instance); } void SetUserNameValue<User,string>(User instance,string newValue) { SetValueDelegate<User,string>(instance,newValue); }
但為了讓我們的方法更通用,再定義點引數和返回值是object型別的屬性讀寫方法:
public object GetValue(object instance) { return GetValueDelegate((T)instance); } public void SetValue(object instance, object newValue) { SetValueDelegate((T)instance, (P)newValue); }
實驗證明,儘管使用了這種方式對引數和返回值進行了型別轉換,但還是要比前面的GetValue、SetValue方法要快得多。現在,將這段程式碼封裝在泛型類 PropertyAccessor<T,P> 中,然後再將屬性的每個GetValueDelegate、SetValueDelegate 快取起來,那麼使用起來效率就很高了:
private INamedMemberAccessor FindAccessor(Type type, string memberName) { var key = type.FullName + memberName; INamedMemberAccessor accessor; accessorCache.TryGetValue(key, out accessor); if (accessor == null) { var propertyInfo = type.GetProperty(memberName); if (propertyInfo == null) throw new ArgumentException("實體類中沒有屬性名為" + memberName + " 的屬性!"); accessor = Activator.CreateInstance(typeof(PropertyAccessor<,>).MakeGenericType(type, propertyInfo.PropertyType)
, type, memberName) as INamedMemberAccessor; accessorCache.Add(key, accessor); } return accessor; }
有了這個方法,看起來讀寫一個屬性很快了,但將它直接放到“百萬級別”的資料查詢場景下,它還是不那麼快,之前老趙有篇文章曾經說過,這個問題有“字典查詢開銷”,不是說用了字典就一定快,因此,我們真正用的時候還得做下處理,把它“暫存”起來,看下面的程式碼:
public static List<T> QueryList<T>(IDataReader reader) where T : class, new() { List<T> list = new List<T>(); using (reader) { if (reader.Read()) { int fcount = reader.FieldCount; INamedMemberAccessor[] accessors = new INamedMemberAccessor[fcount]; DelegatedReflectionMemberAccessor drm = new DelegatedReflectionMemberAccessor(); for (int i = 0; i < fcount; i++) { accessors[i] = drm.FindAccessor<T>(reader.GetName(i)); } do { T t = new T(); for (int i = 0; i < fcount; i++) { if (!reader.IsDBNull(i)) accessors[i].SetValue(t, reader.GetValue(i)); } list.Add(t); } while (reader.Read()); } } return list; }
上面的程式碼,每次查詢到屬性訪問器之後,drm.FindAccessor<T>(reader.GetName(i)),把它按照順序位置存入一個數組中,在每次讀取DataReader的時候,按照陣列索引拿到當前位置的屬性訪問器進行操作:
accessors[i].SetValue(t, reader.GetValue(i));
無疑,陣列按照索引訪問,速度比字典要來得快的,字典每次得計算Key的雜湊值然後再根據索引定位的。
就這樣,我們採用 泛型委託+反射+快取的方式,終於實現了一個快速的ORM,PDF.NET Ver 5.0.3 加入了該特性,使得框架支援POCO實體類的效果更好了。
2.2,表示式樹
有關表示式樹的問題,我摘引下別人文章中的段落,原文在《表示式即編譯器》:
微軟在.NET 3.5中引入了LINQ。LINQ的關鍵部分之一(尤其是在訪問資料庫等外部資源的時候)是將程式碼表現為表示式樹的概念。這種方法的可用領域非常廣泛,例 如我們可以這樣篩選資料:
var query = from cust in customers
where cust.Region == "North"
select cust;
雖然從程式碼上看不太出彩,但是它和下面使用Lambda表示式的程式碼是完全一致的:
var query = customers.Where(cust => cust.Region == "North");
LINQ 以及Where方法細節的關鍵之處,便是Lambda表示式。在LINQ to Object中,Where方法接受一個Func<T, bool>型別的引數——它是一個根據某個物件(T)返回true(表示包含該物件)或false(表示排除該物件)的委託。然而,對於資料庫這樣 的資料來源來說,Where方法接受的是Expression<Func<T, bool>>引數。它是一個表示測試規則的表 達式樹,而不是一個委託。
這裡的關鍵點,在於我們可以構造自己的表示式樹來應對各種不同場景的需求——表示式樹還帶有編譯為一個強型別委託的功能。這讓我們可 以在執行時輕鬆編寫IL。
-------引用完------------
不用說,根正苗紅的Linq2Sql,EntityFramework,都是基於表示式樹打造的ORM。現在,EF也開源了,感興趣的朋友可以去看下它在DataReader讀取資料的時候是怎麼MAP到實體類的。
2.3,Emit
現在很多聲稱速度接近手寫的ORM框架,都利用了Emit技術,比如前面說的微型ORM代表Dapper。下面,我們看看Dapper是怎麼具體使用Emit來讀寫實體類的。
/// <summary> /// Read the next grid of results /// </summary> #if CSHARP30 public IEnumerable<T> Read<T>(bool buffered) #else public IEnumerable<T> Read<T>(bool buffered = true) #endif { if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); if (consumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); var typedIdentity = identity.ForGrid(typeof(T), gridIndex); CacheInfo cache = GetCacheInfo(typedIdentity); var deserializer = cache.Deserializer; int hash = GetColumnHash(reader); if (deserializer.Func == null || deserializer.Hash != hash) { deserializer = new DeserializerState(hash, GetDeserializer(typeof(T), reader, 0, -1, false)); cache.Deserializer = deserializer; } consumed = true; var result = ReadDeferred<T>(gridIndex, deserializer.Func, typedIdentity); return buffered ? result.ToList() : result; }
在上面的方法中,引用了另外一個方法 GetDeserializer(typeof(T), reader, 0, -1, false) ,再跟蹤下去,這個方法裡面大量使用Emit方式,根據實體類型別T和當前的DataReader,構造合適的程式碼來快速讀取資料並賦值給實體類,程式碼非常多,難讀難懂,感興趣的朋友自己慢慢去分析了。
據說,泛型委託的效率低於表示式樹,表示式樹的效率接近Emit,那麼,使用了Emit,Dapper是不是最快的ORM呢?不能人云亦云,實踐是檢驗真理的唯一標準!
3,華山論劍
3.1 ,參賽陣容
前面,有關ORM的實現原理說得差不多了,現在我們來比試非ORM,ORM它們到底誰才是“武林高手”。首先,對今天參賽選手分門別類:
MS派:
老當益壯--DataSet、強型別DataSet,非ORM
如日中天--Entity Framework 5.0,ORM
西部牛仔派:
身手敏捷--Dapper,ORM
草根派:
大成拳法--PDF.NET,混合型
獨孤派:
藐視一切ORM,手寫最靠譜
3.2,比賽內容
首先,在比賽開始前,會由EF的Code First 功能自動建立一個Users表,然後由PDF.NET 插入100W行隨機的資料。最後,比賽分為2個時段,
第一時段,序列比賽,各選手依次進入賽場比賽,總共比賽10次;
比賽內容為,各選手從這100W行資料中查詢身高大於1.6米的80後,對應的SQL如下:
SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >=1.6 And Birthday>'1980-1-1
各選手根據這個比賽題目,盡情發揮,只要查詢到這些指定的資料即可。
第二時段,並行比賽,每次有3位選手一起進行比賽,總共比賽100次,以平均成績論勝負;
比賽內容為,查早身高在1.6-1.8之間的80後男性,對應的SQL如下:
SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height between 1.6 and 1.8 and sex=1 And Birthday>'1980-1-1'
比賽場館由SqlServer 2008 贊助。
3.3,武功介紹
下面,我們來看看各派系的招式:
3.3.1,EF的招式:不用解釋,大家都看得懂
int count = 0; using (var dbef = new LocalDBContex()) { var userQ = from user in dbef.Users where user.Height >= 1.6 && user.Birthday>new DateTime(1980,1,1) select new { UID = user.UID, Sex = user.Sex, Height = user.Height, Birthday = user.Birthday, Name = user.Name }; var users = userQ.ToList(); count = users.Count; }
3.3.1,DataSet 的招式:這裡分為2部分,前面是弱型別的DataSet,後面是強型別的DataSet
private static void TestDataSet(string sql, AdoHelper db, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); //DataSet sw.Reset(); Console.Write("use DataSet,begin..."); sw.Start(); DataSet ds = db.ExecuteDataSet(sql, CommandType.Text, new IDataParameter[] { db.GetParameter("@height", 1.6), db.GetParameter("@birthday", new DateTime(1980, 1, 1)) }); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", ds.Tables[0].Rows.Count, sw.ElapsedMilliseconds); //System.Threading.Thread.Sleep(100); //使用強型別的DataSet sw.Reset(); Console.Write("use Typed DataSet,begin..."); sw.Start(); // DataSet1 ds1 = new DataSet1(); SqlServer sqlServer = db as SqlServer; sqlServer.ExecuteTypedDataSet(sql, CommandType.Text, new IDataParameter[] { db.GetParameter("@height", 1.6), db.GetParameter("@birthday", new DateTime(1980, 1, 1)) } ,ds1 ,"Users"); sw.Stop(); //下面的方式使用強型別DataSet,但是沒有制定查詢條件,可能資料量會很大,不通用 //DataSet1.UsersDataTable udt = new DataSet1.UsersDataTable(); //DataSet1TableAdapters.UsersTableAdapter uta = new DataSet1TableAdapters.UsersTableAdapter(); //uta.Fill(udt); Console.WriteLine("end,row count:{0},used time(ms){1}", ds.Tables[0].Rows.Count, sw.ElapsedMilliseconds); }
3.3.3,手寫程式碼:根據具體的SQL,手工寫DataReader的資料讀取程式碼,賦值給實體類
//AdoHelper 格式化查詢 IList<UserPoco> list4 = db.GetList<UserPoco>(reader => { return new UserPoco() { UID = reader.GetInt32(0), Sex = reader.GetBoolean(1),//安全的做法應該判斷reader.IsDBNull(i) Height = reader.GetFloat(2), Birthday = reader.GetDateTime(3), Name = reader.IsDBNull(0) ? null : reader.GetString(4) }; }, "SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >={0} And Birthday>{1}", 1.6f,new DateTime(1980,1,1) );
3.3.4,採用泛型委託:直接使用SQL查詢得到DataReader,在實體類MAP的時候,此用泛型委託的方式處理,即文章開頭說明的原理
private static void TestAdoHelperPOCO(string sql, AdoHelper db, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use PDF.NET AdoHelper POCO,begin..."); sw.Start(); List<UserPoco> list = AdoHelper.QueryList<UserPoco>( db.ExecuteDataReader(sql, CommandType.Text, new IDataParameter[] { db.GetParameter("@height", 1.6), db.GetParameter("@birthday", new DateTime(1980, 1, 1)) }) ); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list.Count, sw.ElapsedMilliseconds); }
3.3.5,PDF.NET Sql2Entity:直接使用SQL,但將結果對映到PDF.NET的實體類
List<Table_User> list3 = EntityQuery<Table_User>.QueryList( db.ExecuteDataReader(sql, CommandType.Text,new IDataParameter[] { db.GetParameter("@height", 1.6), db.GetParameter("@birthday", new DateTime(1980, 1, 1)) }) );
3.3.6,IDataRead實體類:在POCO實體類的基礎上,實現IDataRead介面,自定義DataReaer的讀取方式
private static void TestEntityQueryByIDataRead(string sql, AdoHelper db, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use PDF.NET EntityQuery, with IDataRead class begin..."); sw.Start(); List<UserIDataRead> list3 = EntityQuery.QueryList<UserIDataRead>( db.ExecuteDataReader(sql, CommandType.Text, new IDataParameter[] { db.GetParameter("@height", 1.6), db.GetParameter("@birthday", new DateTime(1980, 1, 1)) }) ); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list3.Count, sw.ElapsedMilliseconds); }
其中用到的實體類的定義如下:
public class UserIDataRead : ITable_User, PWMIS.Common.IReadData { //實現介面的屬性成員程式碼略 public void ReadData(System.Data.IDataReader reader, int fieldCount, string[] fieldNames) { for (int i = 0; i < fieldCount; i++) { if (reader.IsDBNull(i)) continue; switch (fieldNames[i]) { case "UID": this.UID = reader.GetInt32(i); break; case "Sex": this.Sex = reader.GetBoolean(i); break; case "Height": this.Height = reader.GetFloat(i); break; case "Birthday": this.Birthday = reader.GetDateTime(i); break; case "Name": this.Name = reader.GetString(i); break; } } } }
3.3.7,PDF.NET OQL:使用框架的ORM查詢API--OQL進行查詢
private static void TestEntityQueryByOQL(AdoHelper db, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use PDF.NET OQL,begin..."); sw.Start(); Table_User u=new Table_User (); OQL q = OQL.From(u) .Select(u.UID, u.Sex, u.Birthday, u.Height, u.Name) .Where(cmp => cmp.Property(u.Height) >= 1.6 & cmp.Comparer(u.Birthday,">",new DateTime(1980,1,1))) .END; List<Table_User> list3 = EntityQuery<Table_User>.QueryList(q, db); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list3.Count, sw.ElapsedMilliseconds); }
3.3.8,PDF.NET OQL&POCO:使用OQL構造查詢表示式,但是將結果對映到一個POCO實體類中,使用了泛型委託
private static void TestEntityQueryByPOCO_OQL(AdoHelper db, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use PDF.NET OQL with POCO,begin..."); sw.Start(); Table_User u = new Table_User(); OQL q = OQL.From(u) .Select(u.UID, u.Sex, u.Birthday, u.Height, u.Name) .Where(cmp => cmp.Property(u.Height) >= 1.6 & cmp.Comparer(u.Birthday, ">", new DateTime(1980, 1, 1))) .END; List<UserPoco> list3 = EntityQuery.QueryList<UserPoco>(q, db); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list3.Count, sw.ElapsedMilliseconds); }
3.3.9,PDF.NET SQL-MAP:將SQL寫在XML配置檔案中,並自動生成DAL程式碼
首先看呼叫程式碼:
private static void TestSqlMap(System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use PDF.NET SQL-MAP,begin..."); sw.Start(); DBQueryTest.SqlMapDAL.TestClassSqlServer tcs = new SqlMapDAL.TestClassSqlServer(); List<Table_User> list10 = tcs.QueryUser(1.6f,new DateTime(1980,1,1)); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list10.Count, sw.ElapsedMilliseconds); }
然後看看對應的DAL程式碼:
//使用該程式前請先引用程式集:PWMIS.Core,並且下面定義的名稱空間字首不要使用PWMIS,更多資訊,請檢視 http://www.pwmis.com/sqlmap // ======================================================================== // Copyright(c) 2008-2010 公司名稱, All Rights Reserved. // ======================================================================== using System; using System.Data; using System.Collections.Generic; using PWMIS.DataMap.SqlMap; using PWMIS.DataMap.Entity; using PWMIS.Common; namespace DBQueryTest.SqlMapDAL { /// <summary> /// 檔名:TestClassSqlServer.cs /// 類 名:TestClassSqlServer /// 版 本:1.0 /// 建立時間:2013/10/3 17:19:07 /// 用途描述:測試SQL-MAP /// 其它資訊:該檔案由 PDF.NET Code Maker 自動生成,修改前請先備份! /// </summary> public partial class TestClassSqlServer : DBMapper { /// <summary> /// 預設建構函式 /// </summary> public TestClassSqlServer() { Mapper.CommandClassName = "TestSqlServer"; //CurrentDataBase.DataBaseType=DataBase.enumDataBaseType.SqlServer; Mapper.EmbedAssemblySource="DBQueryTest,DBQueryTest.SqlMap.config";//SQL-MAP檔案嵌入的程式集名稱和資源名稱,如果有多個SQL-MAP檔案建議在此指明。 } /// <summary> /// 查詢指定身高的使用者 /// </summary> /// <param name="height"></param> /// <returns></returns> public List<LocalDB.Table_User> QueryUser(Single height, DateTime birthday) { //獲取命令資訊 CommandInfo cmdInfo=Mapper.GetCommandInfo("QueryUser"); //引數賦值,推薦使用該種方式; cmdInfo.DataParameters[0].Value = height; cmdInfo.DataParameters[1].Value = birthday; //引數賦值,使用命名方式; //cmdInfo.SetParameterValue("@height", height); //cmdInfo.SetParameterValue("@birthday", birthday); //執行查詢 return EntityQuery<LocalDB.Table_User>.QueryList( CurrentDataBase.ExecuteReader(CurrentDataBase.ConnectionString, cmdInfo.CommandType, cmdInfo.CommandText , cmdInfo.DataParameters)); // }//End Function }//End Class }//End NameSpaceSQL-MAP DAL
最後,看看對應的SQL的XML配置檔案:
<?xml version="1.0" encoding="utf-8"?> <!-- PWMIS SqlMap Ver 1.1.2 ,2006-11-22,http://www.pwmis.com/SqlMap/ Config by SqlMap Builder,Date:2013/10/3 --> <SqlMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="SqlMap.xsd" EmbedAssemblySource="DBQueryTest,DBQueryTest.SqlMap.config" > <Script Type="Access" Version="2000,2002,2003" > <CommandClass Name="TestAccess" Class="TestClassAccess" Description="測試SQL-MAP" Interface=""> <Select CommandName="QueryUser" CommandType="Text" Method="" Description="查詢指定身高的使用者" ResultClass="EntityList" ResultMap="LocalDB.Table_User"> <![CDATA[ SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >=#height:Single,Single# And Birthday>#birthday:DateTime# ]]> </Select> </CommandClass> </Script> <Script Type="SqlServer" Version="2008" ConnectionString=""> <CommandClass Name="TestSqlServer" Class="TestClassSqlServer" Description="測試SQL-MAP" Interface=""> <Select CommandName="QueryUser" CommandType="Text" Method="" Description="查詢指定身高的使用者" ResultClass="EntityList" ResultMap="LocalDB.Table_User"> <![CDATA[ SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >=#height:Single,Single# And Birthday>#birthday:DateTime# ]]> </Select> </CommandClass> </Script> </SqlMap>
3.3.10 Dapper ORM:使用Dapper 格式的SQL引數語法,將查詢結果對映到POCO實體類中
private static void TestDapperORM(string sql, System.Diagnostics.Stopwatch sw) { //System.Threading.Thread.Sleep(1000); sw.Reset(); Console.Write("use Dapper ORM,begin..."); sw.Start(); SqlConnection connection = new SqlConnection(MyDB.Instance.ConnectionString); List<UserPoco> list6 = connection.Query<UserPoco>(sql, new { height = 1.6, birthday=new DateTime(1980,1,1) }) .ToList<UserPoco>(); sw.Stop(); Console.WriteLine("end,row count:{0},used time(ms){1}", list6.Count, sw.ElapsedMilliseconds); }
3.3.11 並行測試的招式:由EF,PDF.NET OQL,Dapper ORM參加,使用Task開啟任務。下面是完整的並行測試程式碼
class ParalleTest { /* query sql: * SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height between 1.6 and 1.8 and sex=1 And Birthday>'1980-1-1' */ private long efTime = 0; private long pdfTime = 0; private long dapperTime = 0; private int batch = 100; public void StartTest() { Console.WriteLine("Paraller Test ,begin...."); for (int i = 0; i < batch; i++) { var task1 = Task.Factory.StartNew(() => TestEF()); var task2 = Task.Factory.StartNew(() => TestPDFNetOQL()); var task3 = Task.Factory.StartNew(() => TestDapperORM()); Task.WaitAll(task1, task2, task3); Console.WriteLine("----tested No.{0}----------",i+1); } Console.WriteLine("EF used all time:{0}ms,avg time:{1}", efTime, efTime / batch); Console.WriteLine("PDFNet OQL used all time:{0}ms,avg time:{1}", pdfTime, pdfTime/batch); Console.WriteLine("Dapper ORM used all time:{0}ms,avg time:{1}", dapperTime, dapperTime/batch); Console.WriteLine("Paraller Test OK!"); } public void TestEF() { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); using (var dbef = new LocalDBContex()) { var userQ = from user in dbef.Users where user.Height >= 1.6 && user.Height <= 1.8 //EF 沒有 Between? && user.Sex==true && user.Birthday > new DateTime(1980, 1, 1) select new { UID = user.UID, Sex = user.Sex, Height = user.Height, Birthday = user.Birthday, Name = user.Name }; var users = userQ.ToList(); } sw.Stop(); Console.WriteLine("EF used time:{0}ms.",sw.ElapsedMilliseconds); efTime += sw.ElapsedMilliseconds; } public void TestPDFNetOQL() { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Table_User u = new Table_User() { Sex=true }; OQL q = OQL.From(u) .Select(u.UID, u.Sex, u.Birthday, u.Height, u.Name) .Where(cmp => cmp.Between(u.Height,1.6,1.8) & cmp.EqualValue(u.Sex) & cmp.Comparer(u.Birthday, ">", new DateTime(1980, 1, 1)) ) .END; List<Table_User> list3 = EntityQuery<Table_User>.QueryList(q); sw.Stop(); Console.WriteLine("PDFNet ORM(OQL) used time:{0}ms.", sw.ElapsedMilliseconds); pdfTime += sw.ElapsedMilliseconds; } public void TestDapperORM() { string sql = @"SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height between @P1 and @P2 and sex=@P3 And Birthday>@P4"; System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); SqlConnection connection = new SqlConnection(MyDB.Instance.ConnectionString); List<UserPoco> list6 = connection.Query<UserPoco>(sql, new { P1 = 1.6,P2=1.8,P3=true,P4 = new DateTime(1980, 1, 1) }) .ToList<UserPoco>(); sw.Stop(); Console.WriteLine("DapperORM used time:{0}ms.", sw.ElapsedMilliseconds); dapperTime += sw.ElapsedMilliseconds; } }
3.4,場館準備
為了更加有效地測試,本次測試準備100W行隨機的資料,每條資料的屬性值都是隨機模擬的,包括姓名、年齡、性別、身高等,下面是具體程式碼:
private static void InitDataBase() { //利用EF CodeFirst 自動建立表 int count = 0; var dbef = new LocalDBContex(); var tempUser= dbef.Users.Take(1).FirstOrDefault(); count= dbef.Users.Count(); dbef.Dispose(); Console.WriteLine("check database table [Users] have record count:{0}",count); //如果沒有100萬條記錄,插入該數量的記錄 if (count < 1000000) { Console.WriteLine("insert 1000000 rows data..."); System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); //下面的db 等同於 MyDB.Instance ,它預設取最後一個連線配置 AdoHelper db = MyDB.GetDBHelperByConnectionName("default"); using (var session = db.OpenSession()) { List<Table_User> list = new List<Table_User>(); int innerCount = 0; for (int i = count; i < 1000000; i++) { Table_User user = new Table_User(); user.Name = Util.CreateUserName(); user.Height = Util.CreatePersonHeight(); user.Sex = Util.CreatePersonSex(); user.Birthday =Util.CreateBirthday(); list.Add(user); innerCount++; if (innerCount > 10000) { DataTable dt = EntityQueryAnonymous.EntitysToDataTable<Table_User>(list); SqlServer.BulkCopy(dt, db.ConnectionString, user.GetTableName(), 10000); list.Clear(); innerCount = 0; Console.WriteLine("{0}:inserted 10000 rows .",DateTime.Now); } } if (list.Count > 0) { innerCount=list.Count; DataTable dt = EntityQueryAnonymous.EntitysToDataTable<Table_User>(list); SqlServer.BulkCopy(dt, db.ConnectionString, list[0].GetTableName(), innerCount); list.Clear(); Console.WriteLine("{0}:inserted {1} rows .", DateTime.Now, innerCount); innerCount = 0; } } Console.WriteLine("Init data used time:{0}ms",sw.ElapsedMilliseconds); } Console.WriteLine("check database ok."); }
要使用它,得先準備一下配置檔案了,本測試程式使用EF CodeFirst 功能,所以配置檔案內容有所增加:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.4.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="LocalDBContex" connectionString="Data Source=.;Initial Catalog=LocalDB;Persist Security Info=True;Integrated Security=SSPI;" providerName="System.Data.SqlClient" /> <add name="default" connectionString="Data Source=.;Initial Catalog=LocalDB;Integrated Security=True" providerName="SqlServer" /> <add name="DBQueryTest.Properties.Settings.LocalDBConnectionString" connectionString="Data Source=.;Initial Catalog=LocalDB;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /> </startup> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" /> </entityFramework> </configuration>
系統配置中,要求使用SqlServer資料庫,且實現建立一個數據庫 LocalDB,如果資料庫不在本地機器上,需要修改連線字串。
三、水落石出
經過上面的準備,你是不是已經很急切的想知道誰是絕頂高手了?
EF,它的執行效率長期被人詬病,除了大部分人認為開發效率No.1之外,沒有人相信它會是冠軍了,今天它會不會是匹黑馬呢?
Dapper,身手敏捷,兼有SQL的靈活與ORM的強大,加之它是外國的月亮,用的人越來越多,有點要把EF比下去的架勢,如日中天了!
PDF.NET,本土草根,本著“中國的月亮沒有外國的圓”的傳統觀念,不被看好。
Hand Code,藉助PDF.NET提供的SqlHelper(AdoHelper)來寫的,如果其它人比它還快,那麼一定是運氣太差,否則,其它人都只有唯它“馬首是瞻”的份!
比賽開始,第一輪,序列比賽,下面是比賽結果:
Entityframework,PDF.NET,Dapper Test. Please config connectionStrings in App.config,if OK then continue. check database table [Users] have record count:1000000 check database ok. SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >=1.6 And Birthday>'1980-1-1' -------------Testt No.1---------------- use EF CodeFirst,begin...end,row count:300135,used time(ms)1098 use DataSet,begin...end,row count:300135,used time(ms)2472 use Typed DataSet,begin...end,row count:300135,used time(ms)3427 use PDF.NET AdoHelper (hand code),begin...end,row count:300135,used time(ms)438 use PDF.NET AdoHelper POCO,begin...end,row count:300135,used time(ms)568 use PDF.NET EntityQuery,begin...end,row count:300135,used time(ms)538 use PDF.NET EntityQuery, with IDataRead class begin...end,row count:300135,used time(ms)432 use PDF.NET OQL,begin...end,row count:300135,used time(ms)781 use PDF.NET OQL with POCO,begin...end,row count:300135,used time(ms)639 use PDF.NET SQL-MAP,begin...end,row count:300135,used time(ms)577 use Dapper ORM,begin...end,row count:300135,used time(ms)1088
-------------Testt No.2---------------- use EF CodeFirst,begin...end,row count:300135,used time(ms)364 use DataSet,begin...end,row count:300135,used time(ms)1017 use Typed DataSet,begin...end,row count:300135,used time(ms)3168 use PDF.NET AdoHelper (hand code),begin...end,row count:300135,used time(ms)330 use PDF.NET AdoHelper POCO,begin...end,row count:300135,used time(ms)596 use PDF.NET EntityQuery,begin...end,row count:300135,used time(ms)555 use PDF.NET EntityQuery, with IDataRead class begin...end,row count:300135,used time(ms)445 use PDF.NET OQL,begin...end,row count:300135,used time(ms)555 use PDF.NET OQL with POCO,begin...end,row count:300135,used time(ms)588 use PDF.NET SQL-MAP,begin...end,row count:300135,used time(ms)559 use Dapper ORM,begin...end,row count:300135,used time(ms)534 -------------Testt No.3---------------- use EF CodeFirst,begin...end,row count:300135,used time(ms)346 use DataSet,begin...end,row count:300135,used time(ms)1051 use Typed DataSet,begin...end,row count:300135,used time(ms)3195 use PDF.NET AdoHelper (hand code),begin...end,row count:300135,used time(ms)305 use PDF.NET AdoHelper POCO,begin...end,row count:300135,used time(ms)557 use PDF.NET EntityQuery,begin...end,row count:300135,used time(ms)549 use PDF.NET EntityQuery, with IDataRead class begin...end,row count:300135,used time(ms)456 use PDF.NET OQL,begin...end,row count:300135,used time(ms)664 use PDF.NET OQL with POCO,begin...end,row count:300135,used time(ms)583 use PDF.NET SQL-MAP,begin...end,row count:300135,used time(ms)520 use Dapper ORM,begin...end,row count:300135,used time(ms)543
由於篇幅原因,這裡只貼出前3輪的比賽成績,比賽結果,EF居然是匹黑馬,一雪前恥,速度接近手寫程式碼,但是EF,Dapper,第一輪比賽竟然輸給了PDF.NET OQL,而Dapper後面只是略勝,比起PDF.NET POCO,也是略勝,看來泛型委託還是輸給了Emit,而EF,Dapper,它們在第一執行的時候,需要快取程式碼,所以較慢。多次執行發現,EF僅這一次較慢,以後數次都很快,看來EF的程式碼快取策略,跟Dapper還是不一樣。
但是,Dapper居然輸給了EF,這是怎麼回事?莫非表示式樹比Emit還快?
(完整的比較,請參考這篇正式文章:https://www.cnblogs.com/bluedoctor/p/3378683.html )