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

  • 相关阅读:
    maven打包时出现 Failed to execute goal org.apache.maven.plugins:maven-install-plugin:2.4:install (default-install) on project……
    maven+springmvc出现:java.sql.SQLException: Unknown system variable 'query_cache_size'
    IDEA复制某个类的包名路径
    IDEA中更改Tomcat服务器的URL
    IDEA去除自动检测bean是否存在
    ASM ClassReader failed to parse class file
    The content of element type "web-app" must match "(icon?,display-name?,description?,distributable?,context-param*,filter*,filter-mapping*,listener*,servlet*,servlet- mapping*,session-config?,mime-map
    Could not open ServletContext resource [/WEB-INF/xxx-servlet.xml]
    Eclipse+maven 构建第一个简单的springmvc项目
    Eclispe中编辑xml配置文件时不会提示也不能自动调整格式
  • 原文地址:https://www.cnblogs.com/bluedoctor/p/3364972.html
Copyright © 2020-2023  润新知