1. 程式人生 > 實用技巧 >用事實說話,成熟的ORM效能不是瓶頸,靈活性不是問題:EF5.0、PDF.NET5.0、Dapper原理分析與測試手記

用事實說話,成熟的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 NameSpace 
SQL-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