(转)用事实说话,成熟的ORM性能不是瓶颈,灵活性不是问题:EF5.0、PDF.NET5.0、Dapper原理分析与测试手记

原文地址:http://www.cnblogs.com/bluedoctor/p/3378683.html

[本文篇幅较长,可以通过目录查看您感兴趣的内容,或者下载格式良好的PDF版本文件查看] 

目录

一、ORM的"三国志"    2

1,PDF.NET诞生历程    2

2,Linq2Sql&EF:    3

3,微型ORM崛起    4

二、一决高下    4

2.1,ORM没有DataSet快?    4

2.1.1,ORM生成SQL的质量问题    4

2.1.2,DataReader没有DataSet快?    5

2,ORM的三个火枪手    6

2.1,委托+缓存    6

2.2,表达式树    11

2.3,Emit    12

三、华山论剑    14

3.1,参赛阵容    14

3.2,比赛内容    14

3.3,武功介绍    15

3.3.1,EF的招式:    15

3.3.1,DataSet 的招式:    16

3.3.3,手写代码:    18

3.3.4,采用泛型委托:    18

3.3.7,PDF.NET OQL:    21

3.3.8,PDF.NET OQL&POCO:    22

3.3.9,PDF.NET SQL-MAP:    23

3.3.10 Dapper ORM:    25

3.3.11 并行测试的招式:    26

3.4,场馆准备    29

四、水落石出    33

4.1,谁可能是高手?    33

4.2,串行测试    33

4.3,并行测试    37

4.4,小结    39

 一、ORM的"三国志"

1,PDF.NET诞生历程

  记得我很早以前i(大概05年以前),刚听到ORM这个词的时候,就听到有人在说ORM性能不高,要求性能的地方都是直接SQL的,后来谈论ORM的人越来越多的时候,我也去关注了下,偶然间发现,尼玛,一个文章表的实体类,居然查询的时候把Content(内容)字段也查询出来了,这要是我查询个文章列表,这些内容字段不仅多余,而且严重影响性能,为啥不能只查询我需要的字段到ORM?自此对ORM没有好感,潜心研究SQL去了,将SQL封装到一个XML文件程序再来调用,还可以在运行时修改,别提多爽了,ORM,一边去吧:)

  到了06年,随着这种写SQL的方式,我发现一个项目里面CRUDSQL实在是太多了,特别是分页,也得手写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的用户的IDNameSex 信息,当然User 实体类还有其它属性,当前只需要这几个属性。

  当时这个ORM查询API--OQL很简单,只能处理相等条件的查询,但是能够只选取实体类的部分属性,已经很好了,复杂点的查询,结合在XML中写SQL语句的方式解决,其它一些地方,通过数据控件,直接生成SQL语句去执行了,比如用数据控件来更新表单数据到数据库。

  小结一下我做CRUD的历史,首先是对写SQL乐此不彼,还发明了在XML文件中配置SQL然后映射到程序的功能:SQL-MAP,然后觉得这样写SQL尽管方便管理编写查询且可以自动生成DAL代码,但是项目里面大量的SQL还是导致工作量很大,于是拿起ORM并发明了查询部分实体类属性的查询APIOQL;最后,觉得有些地方用ORM还是麻烦,比如处理一个表单的CRUD,如果用ORM也得收集或者填充数据到实体类上,还不如直接发出SQL,于是又有了"数据控件"这样,按照出现的顺序,在200611月,一个具有SQL-MAPORMData Control功能的数据开发框架:PDF.NET Ver 1.0 诞生了!

2,Linq2Sql&EF:

  2008年,随着.NET 3.5VS2008发布,MS的官方ORM框架Linq2Sql也一同发布了,它采用Linq语法来查询数据库,也就是说LinqMSORM查询API。由于Linq语法跟SQL语法有较大的区别,特别是Linq版本的左、又连接查询语法,跟SQLJoin连接查询,差异巨大,因此,学习Linq需要一定的成本。但是,LINQ to SQL是一个不再更新的技术。其有很多不足之处,如,不能灵活的定义对象模型与数据表之间的映射、无法扩展提供程序只能支持SQL Server等。 MS在同年,推出了Entity Framework,大家习惯的简称它为EF,它可以支持更多的数据库。于是在200812月,我原来所在公司的项目经理急切的准备去尝试它,用EF去开发一个用Oracle的系统。到了20098月,坊间已经到处流传,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框架的速度接近手写映射了。这类框架的代表就是DapperPetaPOCO.

二、一决高下

2.1,ORM没有DataSet快?

2.1.1,ORM生成SQL的质量问题

 这个问题由来已久,自ORM诞生那一天起就有不少人在疑问,甚至有人说,复杂查询,就不该用ORM(见《为什么不推崇复杂的ORM 》,不仅查询语法不灵活,性能也底下。对此问题,我认为不管是Linq,还是OQL,或者是别的什么ORM的查询API,要做到SQL那么灵活的确不可能,所以Hibernate还有HQLEF还有ESQL,基于字符串的实体查询语句,但我觉得既然都字符串了还不如直接SQL来的好;而对于复杂查询效率低的问题,这个跟ORM没有太大关系,复杂查询哪怕用SQL写,DB执行起来也低的,ORM只不过自动生成SQLDB去执行而已,问题可能出在某些ORM框架输出的SQL并不是开发人员预期的,也很难对它输出的SQL语句进行优化,从而导致效率较低,但这种情况并不多见,不是所有的查询ORM输出的SQL都很烂,某些SQL还是优化的很好的,只不过优化权不开在发人员手中。另外,有的ORM语言可以做到查询透明化的,即按照你用ORM的预期去生成对应的SQL,不会花蛇添足,PDF.NETORM查询语言OQL就是这样的。

2.1.2,DataReader没有DataSet快?

  那么,对于一般的查询,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的三个火枪手

  要让ORM具有实用价值,那么必须解决性能问题,经过前面的分析,我们知道问题不在于DataReader本身是否比DataSet慢,而在于DataReader合适的数据读取方式与读取的值赋值给实体类的效率问题。前者已经有很多文章分析过,使用索引定位DataReader并进行类型化的读取是效率的关键,在本文"3.3.3,手写代码"里面做了最佳实践的示例;后者的问题,我们必须找到"三个火枪手",来看他们如何避开直接反射,最快的实现给对象赋值?就是我们今天要讨论的问题。

  OK,我们就以3个流行的框架,采用3种不同的方式实现的ORM ,来比较下看谁的效率最高。在比赛前,我们先分别看看3ORM的实现方式。

2.1,委托+缓存

  我们首先得知,对一个属性进行读写,可以通过反射实现,如下面的代码:

PropertyInfo.GetValue(source,null);

PropertyInfo.SetValue(target,Value ,null);

  PropertyInfo 是对象的属性信息对象,可以通过反射拿到对象的每个属性的属性信息对象,我们可以给它定义一个委托来分别对应属性的读写:

public Func<object, object[], object> Getter { get; privateset; }

public Action<object, object, object[]> Setter { get; privateset; }

  我们将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.GetValuePropertyInfo.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类型的属性读写方法:

publicobject GetValue(object instance)

{

return GetValueDelegate((T)instance);

}

 

publicvoid SetValue(object instance, object newValue)

{

SetValueDelegate((T)instance, (P)newValue);

}

  实验证明,尽管使用了这种方式对参数和返回值进行了类型转换,但还是要比前面的GetValueSetValue方法要快得多。现在,将这段代码封装在泛型类 PropertyAccessor<T,P> 中,然后再将属性的每个GetValueDelegateSetValueDelegate 缓存起来,那么使用起来效率就很高了:

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)

thrownew ArgumentException("实体类中没有属性名为" + memberName + " 的属性!");

accessor = Activator.CreateInstance(

typeof(PropertyAccessor<,>).MakeGenericType(type, propertyInfo.PropertyType)

, type

, memberName

) as INamedMemberAccessor;

accessorCache.Add(key, accessor);

}

 

return accessor;

}

  有了这个方法,看起来读写一个属性很快了,但将它直接放到"百万级别"的数据查询场景下,它还是不那么快,之前老赵有篇文章曾经说过,这个问题有"字典查询开销",不是说用了字典就一定快,因此,我们真正用的时候还得做下处理,把它"暂存"起来,看下面的代码:

publicstatic 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的哈希值然后再根据索引定位的。

就这样,我们采用泛型委托+反射+缓存的方式,终于实现了一个快速的ORMPDF.NET Ver 5.0.3 加入了该特性,使得框架支持POCO实体类的效果更好了。

2.2,表达式树

有关表达式树的问题,我摘引下别人文章中的段落,原文在《表达式即编译器》:

微软在.NET 3.5中引入了LINQLINQ的关键部分之一(尤其是在访问数据库等外部资源的时候)是将代码表现为表达式树的概念。这种方法的可用领域非常广泛,例如我们可以这样筛选数据:

var query = fromcustin 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

 -------引用完------------

不用说,根正苗红的Linq2SqlEntityFramework,都是基于表达式树打造的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) thrownew ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed");

if (consumed) thrownew 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,那么,使用了EmitDapper是不是最快的ORM呢?不能人云亦云,实践是检验真理的唯一标准!

三、华山论剑

3.1,参赛阵容

前面,有关ORM的实现原理说得差不多了,现在我们来比试非ORMORM它们到底谁才是"武林高手"。首先,对今天参赛选手分门别类:

MS派:

  • 老当益壮--DataSet、强类型DataSet,ORM
  • 如日中天--Entity Framework 5.0,ORM

西部牛仔派:

  • 身手敏捷--Dapper,ORM

草根派:

  • 大成拳法--PDF.NET,混合型

独孤派:

  • 藐视一切ORM,手写最靠谱

3.2,比赛内容

  首先,在比赛开始前,会由EFCode First 功能自动创建一个Users表,然后由PDF.NET 插入100W行随机的数据。最后,比赛分为2个时段,

第一时段,串行比赛,各选手依次进入赛场比赛,总共比赛10次;

比赛内容为,各选手从这100W行数据中查找身高大于1.6米的80后,对应的SQL如下:

SELECT UID,Sex,Height,Birthday,Name FROM Users Where Height >=1.6And Birthday>'1980-1-1

各选手根据这个比赛题目,尽情发挥,只要查询到这些指定的数据即可。

第二时段,并行比赛,每次有3位选手一起进行比赛,总共比赛100次,以平均成绩论胜负;

比赛内容为,查早身高在1.6-1.8之间的80后男性,对应的SQL如下:

SELECT UID,Sex,Height,Birthday,Name FROM Users

Where Height between1.6and1.8and sex=1And 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)

selectnew

{

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

privatestaticvoid 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 =>

{

returnnew 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的时候,此用泛型委托的方式处理,即文章开头说明的原理

privatestaticvoid 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.6IDataRead实体类:在POCO实体类的基础上,实现IDataRead接口,自定义DataReaer的读取方式

privatestaticvoid 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);

 

}

其中用到的实体类的定义如下:

publicclass UserIDataRead : ITable_User, PWMIS.Common.IReadData

{

//实现接口的属性成员代码略

publicvoid 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进行查询

privatestaticvoid 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实体类中,使用了泛型委托

privatestaticvoid 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代码首先看调用代码:

privatestaticvoid 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>

publicpartialclass 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

最后,看看对应的SQLXML配置文件:

<?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实体类中

privatestaticvoid 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 并行测试的招式:

EFPDF.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'

*/

 

privatelong efTime = 0;

privatelong pdfTime = 0;

privatelong dapperTime = 0;

privateint batch = 100;

 

publicvoid 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!");

}

 

publicvoid 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)

selectnew

{

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;

}

 

publicvoid 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;

}

 

publicvoid 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行随机的数据,每条数据的属性值都是随机模拟的,包括姓名、年龄、性别、身高等,下面是具体代码:

privatestaticvoid 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,如果数据库不在本地机器上,需要修改连接字符串。

   

四、水落石出

4.1,谁可能是高手?

经过上面的准备,你是不是已经很急切的想知道谁是绝顶高手了?

  • EF,它的执行效率长期被人诟病,除了大部分人认为开发效率No.1之外,没有人相信它会是冠军了,今天它会不会是匹黑马呢?
  • Dapper,身手敏捷,兼有SQL的灵活与ORM的强大,加之它是外国的月亮,用的人越来越多,有点要把EF比下去的架势,如日中天了!
  • PDF.NET,本土草根,本着"中国的月亮没有外国的圆"的传统观念,不被看好。
  • Hand Code,借助PDF.NET提供的SqlHelperAdoHelper)来写的,如果其它人比它还快,那么一定是运气太差,否则,其它人都只有唯它"马首是瞻"的份!

4.2,串行测试

比赛开始,第一轮,串行比赛,下面是比赛结果:

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居然是匹黑马,一雪前耻,速度接近手写代码,但是EFDapper,第一轮比赛竟然输给了PDF.NET OQL,Dapper后面只是略胜,比起PDF.NET POCO,也是略胜,看来泛型委托还是输给了Emit,而EF,Dapper,它们在第一运行的时候,需要缓存代码,所以较慢。多次运行发现,EF仅这一次较慢,以后数次都很快,看来EF的代码缓存策略,跟Dapper还是不一样。

但是,Dapper居然输给了EF,这是怎么回事?莫非表达式树比Emit还快?还是EF将结果缓存了?使用SqlServer事务探察器,发现EF的确每次发出了查询,没有缓存数据。看来EF5.0的表达式树可能真是效率有了很大提升,并且EF做了很好的优化,对EF取得的成果,不得不叹服!

下面是10次串行测试的数据表:

数据访问 模式

EF CodeFirst

DataSet

Typed  DataSet

PDF.NET  AdoHelper HandCode

PDF.NET  AdoHelper POCO

PDF.NET  EntityQuery

PDF.NET  IDataRead

PDF.NET  OQL

PDF.NET  OQL with POCO

PDF.NET  SQL-MAP

Dapper ORM

1

374

1083

3220

305

564

584

449

598

585

601

516

2

344

1005

3188

314

549

545

445

578

574

564

493

3

341

1001

3110

287

527

584

436

624

743

560

486

4

342

1007

3380

342

574

575

539

758

655

633

638

5

424

1176

3275

318

659

855

553

684

797

679

647

6

433

1137

3156

283

536

588

441

546

628

540

487

7

356

1044

3671

301

537

536

452

524

603

516

479

8

367

975

3123

340

514

552

506

599

583

598

480

9

385

993

3213

306

538

612

473

569

596

545

521

10

483

987

3125

295

606

597

475

545

601

509

523

平均

384.9

1040.8

3246.1

309.1

560.4

602.8

476.9

602.5

636.5

574.5

527

由于Type DataSet差异过大,在下面的图中去掉了:

 

下面是平均值图形:

 

4.3,并行测试

下面是并行测试结果,程序共运行100次,每次三种ORM框架同时运行。由于篇幅原因,这里只贴出最后三次的测试数据和最后计算的每种框架的性能平均数。

----tested No.97----------

DapperORM used time:435ms.

EF used time:462ms.

PDFNet ORM(OQL) used time:466ms.

----tested No.98----------

PDFNet ORM(OQL) used time:383ms.

DapperORM used time:389ms.

EF used time:451ms.

----tested No.99----------

PDFNet ORM(OQL) used time:390ms.

DapperORM used time:445ms.

EF used time:456ms.

----tested No.100----------

EF used all time:44462ms,avg time:444

PDFNet OQL used all time:38850ms,avg time:388

Dapper ORM used all time:39127ms,avg time:391

Paraller Test OK!

经过数次测试发现,三种框架表现都很优异,差距很小,其中PDF.NET OQL略微胜出,Dapper次之,EF稍慢。

ORM框架

EF Code First

PDF.Net OQL

Dapper ORM

并行测试耗时百分比

35.45

32.18

32.35

 

4.4,小结

  串行测试,EF5.0 胜出,并行测试,PDF.NET胜出。在实际运行环境中,并行测试可能更好的反映问题。但是两种测试场景,它们各自之见的性能并没有"量"的差距,因此我们选择具体的成熟的ORM的时候,性能并不是一个主要考察依据,而应该综合分析比较,比如以下问题:

  • 源码规模是否较小,容易掌握;
  • 源码是否有充分的注释,对于国人而言是否有完整的中文注释;
  • 源码是否符合国人的习惯,简单,易于理解;
  • 使用是否方便,用户代码是否最少;
  • 是否能够支持.NET 2.0 框架;
  • 是否能够跨平台,比如在Linux上运行;
  • 是否支持所有的Ado.NET支持的数据库;
  • 是否有社区支持,能够得到框架团队的直接支持。

   

 

 

 

附注

为什么要选择PDF.NET框架?

  • 喜欢简单的开发过程!
  • 老板逼的急,要快速开发完成!
  • 喜欢ORM框架!
  • 喜欢存储过程+手写SQL的开发方式,更有"安全感"
  • 希望像写SQL那样来操作ORM框架!
  • EFNH等框架在我的项目中某些特性没法完全满足,想定制修改或者扩展,但没有源码或者源码规模巨大!
  • 程序复杂,执行要快!
  • 团队中有人喜欢拖控件,有人喜欢写SQL,也有人喜欢ORM,众口难调,作为Leader,不知道如何是好!
  • 项目大,为确保成功,需要掌控所有的细节,包括ORM等数据访问的细节!
  • 系统需要移植到新的数据库系统,但原来的系统手写了很多特定数据库平台的SQL语句,没法移植!
  • ...

   框架是我们多年开发经验的总结,在众多流行的开发框架下,相信你选择PDF.NET没错!

开源基金:

  中国的软件开源事业需要更多的人的关心和支持,PDF.NET为此在2011年开始加入开源行列,并在2012年国庆前对最新版本进行开源,希望国人在基础开发框架方面有更多的选择,促进中国软件事业的发展。但PDF.NET出身草根,它的发展需要您的更多呵护。如果您觉得它的确为你的软件开发起到了帮助,并且愿意更进一步的支持框架的发展,请捐助PDF.NET,我们将使用这笔资金来进行框架的宣传、推广、培训活动;支付框架所在网站、源代码托管服务;组织开发活动,奖励开发团队的贡献。
    
有关抗震救灾和捐助的详细信息,请参看框架官网
http://www.pwmis.com/sqlmap介绍,请以官网公布的信息为准!感谢所有已经捐助过和关心PDF.NET的朋友,期望他们的爱心能够让更多的人知晓并赞扬!

关于本文:

本文中使用的测试程序下载: https://pwmis.codeplex.com/releases/view/113289

posted on 2013-11-01 12:05  黑子范  阅读(774)  评论(0编辑  收藏  举报

导航