用事实说话,成熟的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代码:

用事实说话,成熟的ORM性能不是瓶颈,灵活性不是问题:EF5.0、PDF.NET5.0、Dapper原理分析与测试手记
//使用该程序前请先引用程序集: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

 

上一篇:day02---统一日志处理(09)


下一篇:利用Eclipse中的Maven构建Web项目(一)