一、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的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 )