ORM之硬伤
园子里有些人,他们真以为自己明白了面向对象,然后装着满腹经纶,侃侃而谈,一篇接一篇,不厌其烦地喊着ORM如何如何。你以为他真的明白“面向对象”么?其实,他对面向对象的理解仅限于教科书中的封装、继承和多态,或者再知道一点面向对象的若干原则但其实并不真正理解。
笔者愚钝,入行多年尚不懂面向对象,只懂得用其形而不懂用其实。五年后的某一天终于开窍,明白了面向对象之实,也仅仅是一个开始而已。当又经历了另一个五年的倦怠,发现并理解了设计模式、面向方面等技术作为面向对象的必要补充后,才算是彻悟!所以当我见过一个同学,尚未出校门已然彻悟,真是羞愧!
有一天面试的时候,我问一位同学,Framework和Library的区别是什么?他答不上来。而另一个同学略一思考就告诉我,你的程序会调用Library,而Framework会调用你的程序。虽然精辟,但我还是要补充:Framework通常也会提供一个Library,所以,Library是水平的,而Framework是垂直的,此处的“水平”和“垂直”是相对应用系统的层次设计而言的。如果没有层次,其实Framework其实就是Library。Microsoft的Enterprise Library当然就是一个Library,无法代替Framework。
如果让那位已经彻悟的同学舍弃ORM来实现复杂的业务功能,他当然无法接受。相反,如果让一位抱着《Thinking in Java》似懂非懂的同学用ORM来实现同样的功能,他也一样无法接受。其中的一些同学非常擅于“鸡蛋里挑骨头”,于是园子里有了这样一堆垃圾文章或者垃圾跟贴。另外一些同学不精于这样的能力,所以仍在徬徨之中。
此乃ORM惟一之硬伤也!如果你不理解面向对象思想,就先试着去理解,然后再来讨论ORM这个话题,并发表你的高见。
再说性能
ORM提供了所有SQL语句的生成,代码人员远离了数据库概念。从一个概念需求(例如一个HQL)映射为一个SQL语句,并不需要什么代价,连1%的性能损失都没有。真正的性能损失在映射过程中,更具体地讲,是在对象实例化的过程中。我曾经做过一个试验,以“计算第N个素数”这样的命题。我采用Delphi写Native Win32 Console程序,又采用C#写CLR Console程序。两者相比,令我大失所望。
N |
结果 |
耗时 |
|
Delphi |
C# |
||
1000 |
7927 |
0ms |
2ms |
10000 |
104743 |
16ms |
17ms |
100000 |
1299721 |
438ms |
324ms |
1000000 |
15485867 |
11437ms |
7823ms |
该命题采用的算法是找出第N个素数以前的所有素数,开辟一个内存区存贮这些素数。在Delphi中我用链表,在C#中我用List<int>。实际的结论是:当列表足够大时,链表的性能远不及List<int>。当然,如果每个链表节点只装一个元素,这种差异会更明显。事实上,我测试过每个链表节点所装的元素个数做了一个阶梯试验,从30个、254个、510个、1022个到2046个,每个节点所装载的元素数越多,耗时越短,最终越来越接近C#的List<int>。
不知道各位是否已经明白了性能在哪儿损失了:内存分配。Native的内存分配与释放都是非常耗时的操作系统行为。但在托管环境下,内存的释放是GC干的事情,甚至不需要统计到耗时中,而内存的分配也是一件非常快捷的事情。当然,即使是快捷也还是需要耗时的。这让我联想到DataSet的性能。DataSet也是一种数据容器,但是却没有多少人抱怨DataSet的性能。如果你明白DataSet的机制,就会发现,DataStorage巧妙地规避了内存分配和耗时的问题。而我们的ORM无法解决每个对象实例在构造时分配内存所耗时间。我做了一个不精确的评估,相比DataSet,对象集合的性能损失大约占20%左右。
如果假定ORM并没有比传统的数据访问方式耗费额外的IO的话,除此之外,ORM再没有任何性能损失!
再回到前提条件:ORM并没有比传统的数据访问方式耗费额外的IO。这个条件成立么?
“由于ORM的实体对象定义已经固定,所以即使我不需要某些字段,也一样需要加载这些字段。”
OK,有的同学已经看出来了。额外定义一个视图的实体对象即可。定义这些视图的实体对象的确很麻烦,但是肯定比构造那些SQL并不断地维护它简单得多。
“当一张表中有1000万行数据时,实例化1000万个对象是不可能的。”
非常正确。难道你曾经成功地尝试过将1000万行数据加载到某个DataTable中并且没有性能问题?从应用的角度来说,在一个模型中包含的实例数超过500行就有设计不当的嫌疑。我对Google的抱怨是:当搜索结果超过1000个时都会令我抓狂。让我从1000行数据中找出我所需要的某一行,这是开发人员的思维,并不是用户的思维。如果能够在已有的结果中进行二次、三次或者多次进一步的筛选,可能更适合绝大多数人。我为什么不愿意在分页中花太多的精力,其原因也是如此。我认为用户的眼球只能接受100行以内的数据,超过这个行数就需要采用其它的方式,或者改善领域设计。所以,这个问题的答案是:你不可能需要一次载入1000万行。
“当应用系统整体性能欠佳时,因为隐藏了数据访问细节,从而无法找到快速优化的途径。”
不能同意。几乎每一个ORM框架都提供了非常可靠的数据库访问日志。通过这些日志分析性能损失将比直接使用SQL语句更可靠、更方便。
灵活性
ORM不够灵活?我完全不能理解,我甚至不知道这个不够灵活是与什么基准相比。相反,ORM可以让你灵活地替换数据库(当然这个优点并没有非常重要的意义);在修改数据库以后不需要修改服务层或者只需要进行简单的修改;可以对某个服务进行单独的测试;可以对服务进行不依赖数据库的、上下文一级的扩展;可以进行更好的层次设计;......
不能实现所有的查询条件
如果是想表达“每一个Select语句可以通过面向对象的方式进行查询”的话,我觉得目前绝大部分ORM框架都已经很好地解决。我解决这一问题的基础是:我不提供超越SQL ANSI92的能力,但覆盖SQL ANSI92的所有功能。对于解决实际应用中的不足部分,采用运行时算法补充。Hibernate采用的是HQL这样的方式,基本上SQL能够做到的,HQL都无一例外可以做到。ECO采用的是OCL的方式,其功能可以完全覆盖SQL。我的框架所实现的查询目前我还没有发现无法解决并必须利用Native SQL来实现的(因此我无法理解Hibernate3为什么要提供这样的扩展)。Hibernate采用的策略是以面向对象为核心,换句话说,以持久化对象为终极目标,而以加载对象以持久化对象为前提。设计一个POJO,实例化,然后保存起来,下次使用的时候可以依样载入即可。大规模的查询并不是框架的核心目标。所以,如果你完全依赖Hibernate去持久化,我非常担心你将来是否有机会用你的数据积累去做数据仓库。而我的框架目标则不同。在持久化与加载两个目标间我没有主次之分。我也没有超前到MDA,我的对象模型仍然基于数据库的ER设计,我仍然提供一组非常清晰明了的数据库视图。
多表连接查询
如果需要将多表的连接查询结果转换成一个二维视图,显然需要你再定义另一个视图实体对象,将视图映射到对象模型。如果你仅仅是要在一个对象实例的某个属性中获得另外一个对象的集合,似乎这不是DAL方式的优势,而反而是ORM的优势。将多个对象所依赖的多个对象放到同一个上下文中,显然这是最好的一种方式。
统计查询
从理论上讲,ORM不适合做OLAP,不适合做太多统计查询。其实这一点,我的框架已经提供了非常好的解决方案,对Aggregate到面向对象的视图处理得非常好。
开发效率
提高开发效率仅仅是一个抽象的目标,具体的手段应该是两个方面:一是IDE和辅助工具;一是适合将任务分解成多个解耦的部分从而可以通过增加人员来提升总的开发效率。虽然ORM仅仅是开发环节中很小的一部分,但是却遍布应用系统中的每一角落,因而对开发效率影响较大。除了ORM,难道还有更好的选择么?
ORM后,原来精湛的SQL技能变得毫无用武之地,让人甚是失落,但这并不是ORM的过错。