什么是反模式?
我很高兴地发现Wikipedia有一个相当全面的关于反模式的列表,包括来自编程界及其之外的内容。我之所以称ORM为反模式的原因是因为,反模式的作者定义了用来区分反模式和普通的坏习惯的两个条件,而ORM完全符合这些条件:
- 它开始的时候看起来很有用,但是从长期来看,坏处要大过好处
- 存在已验证并且可重复的替代方案
由于第一个因素导致了ORM令人抓狂(对我来说)的流行性:它第一眼看上去像是个好主意,但是当问题更加明显的时候,已经很难离开了。
这对ORM来说是什么意思?
我想说的主要问题在于 ActiveRecord,它由于 Ruby on Rails 而著名, 从那以后已经移植到了许多其他语言。然而,这些问题同样存在于其他的ORM层,比如Java的Hibernate和PHP的Doctrine。
ORM的优点
- 简单:一些ORM层告诉你它们“消除了对SQL的要求”。我至今仍然看到这种承诺在传播。其他一些会更加现实地声称它们可以减少手写SQL的需要,但是仍然允许你在需要的时候使用它。对于简单的模型以及项目的早期,这确实是一个优点:使用ORM,无疑你能够更快地开始启动。然而,你将会走向错误的方向。
- 代码生成:使用ORM从模型中消除用户层面的代码,这一做法开启了通向代码生成的大门。通过对schema的简单描述,“脚手架”模式可以为你的所有表生成一个可工作的界面。更加具有魔力的是,你可以修改你的schema描述,然后重新生成代码,从而消除了CRUD。同样,这在开始的时候确实是可行的。
- 性能“足够好”:我没有看到任何ORM层声称在性能上更加优越。很明显,为了代码的敏捷性需要付出性能的代码。如果哪里变慢了,你总是可以用更加有效的手写SQL覆盖你的ORM方法。不是吗?
ORM的问题
1. 不充分的抽象
ORM最明显的问题是它并不能完全从实现细节中抽象出来。所有主流ORM的文档中到处都引用了SQL的概念。其中一些介绍的时候并不会表明其在SQL中的等价物,而其他一些则将库看作用来生成SQL的过程函数。
抽象的要点在于它应该使问题得以简化。对SQL进行抽象,同时又要求你懂得SQL,这使得你需要学习的东西成倍增加了:首先,你必须理解你正在试图执行的SQL是什么,然后你还要学习ORM的API,来让它为你编写这些SQL。在Hibernate中,为了完成复杂的SQL你甚至需要学第三种语言:HQL,它几乎就是SQL(但又不完全是),其在幕后被翻译成SQL。
ORM的支持者会辩解说并非每个项目都是如此,并非每个人都需要复杂的join,并且ORM是一个"80/20"解决方案,其中80%的用户只需要SQL中20%的功能,ORM可以处理这些问题。我能说的是,我15年来编写web应用的数据库后端的经历表明,事实并非如此。只有在项目刚开始的时候你不需要join和本地join。在那之后,你就要优化和巩固你的查询。即使80%的用户只用到SQL中30%的功能,可是100%的用户都需要打破ORM的抽象才能够完成工作。
2. 不正确的抽象
如果你的项目确实不需要任何关系数据功能,那么ORM可以非常完美地为你工作。但是接下来你又遇到另外一个问题:你用错了了数据存储。关系存储的额外付出是非常高的;这就是为什么NoSQL数据要快得多的重要原因之一。然而,如果你的数据是关系型的,那么额外的付出就是值得的:你的数据库不仅存储数据,它还表达了你的数据,并且可以基于关系概念回答关于它的问题,这比你用过程代码能够做到的要快速得多。
但是,如果你的数据不是关系型的,那么你就是在不适当的场合使用SQL,这为你增加了巨大且不必要的负担;为了让问题更加严重,你在其上又增加了一重额外的抽象。
另一方面,如果你的数据是关系型的,那么你的对象映射最终会失败。SQL是关于关系代数的:SQL的输出不是对象,而是对于某个问题的解答。如果你的对象“是一个”X的实例,并且“拥有一些”Y,且每个Y“属于”Z,那么对象在内存中正确的表达形式是什么? 它应该是X的属性,或者全部包含在Y中,或者/并且全部包含在Z中?如果你只得到X的属性,那么何时你运行查询来获得Y呢?而且,你是想要其中一个还是全部?现实中,答案是依赖于条件的:这就是为什么我说SQL是对于问题的回答。对象在内存中的表达形式取决于你的意图,然而面向对象设计没有依赖于上下文的表达这样的功能。关系不是对象;对象也不是关系。
3. 多个查询导致失败
这自然的引出了ORM的另一个问题:效率低下。当你获取一个时,你需要哪些属性?ORM并不知道,所以它总是取得全部(或者它要求你告诉它,但是这又打破了抽象)。开始的时候这不成问题,但是当你一次取出上千条纪录的时候,如果你只需要3个属性却不得不取出全部30列,这时就产生了严重的性能问题。许多ORM层非常不善于推断join,从而不得不使用分离的查询来获取关联数据。如前所述,许多ORM层明确声明效率将会有所牺牲,其中一些提供了某些机制来调整引起问题的查询。我从过去的经历中发现的问题表明,很少有只需要调整单个“银弹”查询的情况:应用的数据库后端之所以死掉不是因为其中某一条查询,而是众多的查询引起的。ORM缺少上下文敏感的性质意味着它无法巩固查询,相反必须借助cache或其他机制来进行一定程度的补偿。
那么替代方案是什么?
希望到这里我已经澄清ORM在设计上的一些缺陷。但是要作为一个反模式,还需要存在替代的解决办法。事实上有两个取代方法:
1. 使用对象
如果你的数据是对象,那么停止使用关系数据库。编程界当前正在出现键-值对存储的浪潮,它允许你以闪电般的速度访问优雅的、自我包含的海量数据。没有法律规定编写Web应用的第一步必须安装MySQL。对于对象的每一种表达方式都使用关系数据库是一种过度使用,这也是近几年SQL的名称不太好的原因之一。事实上,问题在于偷懒的设计。
2. 在模型中使用SQL
编程中作任何事情都只有一种正确的方式,这是一种危险的说法。然而根据我的实践,在面向对象的代码中表达关系模型的最佳方法仍然是模型层:将你的所有数据表示封装在一个单独的区域是一个好注意。然而,记住模型层的工作簿在于表达对象,而在于回答问题。提供一个可以回答你的应用程序所包含的问题的API,尽量保持简洁高效。有时候,这些回答显得格格不入,以致于看上去是“错误的”,甚至对于资深的OO开发者也是如此。但是,你可以根据经验来更好地找到其中的普遍性,从而允许你将多个查询方法重构为单个。
类似的,有时候输出会是单个对象X,它很容易表达。 但是也有时候输出是聚合的对象表格,或者单个整数值。你要忍住将这些内容用过多抽象来包装的诱惑,用对象自身的术语来描述。首要的是,不要相信OO能够表达任何对象和所有对象。OO本身是一种优美和灵活的抽象,但关系数据在其范围之外,把它不能表达的东西伪装成对象是ORM的核心与真正的问题。
总结
- ORM最初比编写基于SQL的模型代码更快,也更容易理解
- 它在任何项目早期都是足够有效的
- 不幸的是,这些优点在项目复杂性提升的时候就消失了:抽象被打破,开发者被迫使用并理解SQL
- 完全是非正式的声明,我认为ORM对抽象的破坏不是仅仅涉及20%的项目,而是几乎100%。
- 对象并不足以充分表达关系查询的结果。
- 关系查询映射到对象的不充分性导致了ORM后端应用的效率低下,这些问题普遍分布在应用的各处,并且除了完全放弃ORM之外,没有简单的解决办法。
- 不要对任何问题都使用关系存储与ORM,而是更加仔细地思考你的设计
- 如果你的数据天生就是对象,那么请使用对象存储("NoSQL")。它们要比关系数据库快得多。
- 如果你的数据天生就是关系型的,那么关系数据库带来的开销是值得的。
- 把你的关系查询封装在模型层中,设计你的API从而为应用提供数据访问支持;拒绝过分泛化的诱惑。
- 面向对象无法以有效的形式表达关系数据;这是面向对象设计的一个基本限制,ORM无法修复它。
作为Java和Ruby程序员与架构师的Yegor昨天发表一篇博文:ORM Is an Offensive Anti-Pattern,认为ORM是一个可怕的反模式,违反了所有的面向对象原则,撕裂了对象,将它们变成哑巴和被动的数据袋,没有任何借口在任何应用程序中使用ORM,无论是成千上万的小型Web应用或企业级的基于数据表的CRUD操作系统(ORM包括Java的Hibernate/JPA,python的django,),那么取而代之是什么?会讲SQL的对象 (SQL-speaking object)。
ORM是如何工作的
对象关系数据库ORM技术或模式是使用面向对象技术如Java访问一个关系数据库,每个语言都有ORM实现,如Java的Hibernate,Ruby的active record, PHP的Doctrine, Python的 SQLAlchemy,在java中,ORM甚至被设计为标准,如JPA。
首先,让我们看看ORM是如何工作的,。 让我们使用Java,PostgreSQL,Hibernate。 假设我们有一个表在数据库中,称为post :
+-----+------------+--------------------------+ | id | date | title | +-----+------------+--------------------------+ | 9 | 10/24/2014 | How to cook a sandwich | | 13 | 11/03/2014 | My favorite movies | | 27 | 11/17/2014 | How much I love my job | +-----+------------+--------------------------+ |
现在,我们需要为这个表产生Java应用的CRUD方式(增删改查),首先,我们曾经一个Post类:
@Entity @Table(name = "post") public class Post { private int id; private Date date; private String title; @Id @GeneratedValue public int get[author]Id[/author]() { return this.id; } @Temporal(TemporalType.TIMESTAMP) public Date getDate() { return this.date; } public Title getTitle() { return this.title; } public void setDate(Date when) { this.date = when; } public void setTitle(String txt) { this.title = txt; } } |
在使用Hibernate操作之前,我们得创建一个session工厂:
SessionFactory factory = new AnnotationConfiguration() .configure() .addAnnotatedClass(Post.class) .buildSessionFactory(); |
工厂每次我们要使用Post对象时产生一个session,每次使用session应当如下使用代码包装:
Session session = factory.openSession(); try { Transaction txn = session.beginTransaction(); // your manipulations with the ORM, see below txn.commit(); } catch (HibernateException ex) { txn.rollback(); } finally { session.close(); } |
当session准备好后,下面我们就可以从数据表中获取所有的post:
List posts = session.createQuery("FROM Post").list(); for (Post post : (List<Post>) posts){ System.out.println("Title: " + post.getTitle()); } |
我认为这是清楚的, Hibernate是一个强大的连接到数据库的引擎,通过执行必要的SQL SELECT请求,获取检索数据。 然后它创建了类Post的实例,并将数据装入其中。 当这个对象过来时,它填满了数据,我们应该使用getter方法将这些数据取出,比如我们使用 getTitle() 方法。
当我们想做一个反向操作,将一个对象发送到数据库,我们做的都基本相同,只不过以相反的顺序。 我们创建类Post的一个实例 文章,然后塞进入数据,请求Hibernate保存它:
Post post = new Post(); post.setDate(new Date()); post.setTitle("How to cook an omelette"); session.save(post); |
这是几乎是每一个ORM工作原理。 基本原则始终是相同的——ORM装有数据的贫血对象。 我们谈论的是ORM框架,这些框架与数据库交互, 对象只有帮助我们将请求发送给ORM框架,并理解其响应。 除了getter和setter,对象没有其他方法。 他们甚至不知道他们来自哪个数据库。
这是对象关系映射是如何工作的。
也许你会问,怎么了?
ORM怎么了?
说真的,有什么错吗? Hibernate是最流行的Java库,已经超过10年了。 世界上几乎每一个SQL-intensive应用程序都是使用它。 每个Java教程会提及Hibernate(或者 其他一些ORM 像TopLink或OpenJPA)用于database-connected应用程序。 它实际上是一个标准, 然而我还要说它错了? 是的。
我声称整个ORM背后的想法是错误的。 它的发明是也许OOP领域空引用之后第二大错误 。
其实,我不是唯一一个说这样的事情的人,绝对不是第一个。 很多非常受人尊敬的作者已经发表关于这个主题,包括 Matinfowler的OrmHate , Jeff Atwood的对象关系映射是计算机科学的越南战争 ,Ted Neward的计算机科学的越南 , Laurie Voss的ORM是一种反模式,等等还有许多其他人。
然而我的观点不同于他们所说的,尽管他们的理由是来自实践且有效,如ORM是慢的,数据库升级很难等,他们错失了主要点,你能从Bozhidar Bozhanov的ORM Haters Don’t Get It文章中看到非常好的实战回答。
ORM主要点并不是封装数据库交互到一个对象,释放数据,遍历时撕开了坚固且聚合的living organism(实体),对象的一部分保持数据,而另外一部分是在ORM引擎(session factory)内部执行,这些引擎知道如何处理这些数据,并且转换它到关系数据库,看看这张图,它模拟了ORM如何工作:
我作为Post文章的读者,得和两个组件打交道,1是ORM,2是返回一个砍了头的对象,我打交道的行为应该有一个单点(操作一个组件),这个对象才是OOP,而在ORM这种情况下,我得与两个点打交道,ORM和数据对象,甚至我们都不能称之为对象。
因为这个可怕的和明显地违反了面向对象范式,我们已经有很多实际中受人尊敬的出版物都在提到这个问题,这里举例一些:
SQL并没有隐藏,ORM的用户应该会使用SQL(或者方言如HQL),看看上面案例,我们调用session.createQuery("FROM Post")是为了获得所有文章Posts,即使它不是SQL,也很类型,关系模型并没有被封装到对象中,相反,它暴露在整个应用程序中,使用这个对象的每个人都必须与关系数据库打交道,以获得或保存什么,这样ORM并没有隐藏和封装SQL,而是污染了整个应用程序。
难以测试。当一些对象要与Posts文章列表交互时,它需要处理一个实例 SessionFactory 。 我们如何在测试中模拟这种依赖性? 我们必须创建一个它的模拟吗? 这个任务有多复杂? 看看上面的代码,你将意识到冗长和繁琐的单元测试。 相反,我们可以编写集成测试并将整个应用程序连接到一个测试版本的PostgreSQL。 在这种情况下,没有必要模拟一个 SessionFactory ,但这种测试将会相当缓慢,更重要的是,我们却将一个并没有和数据库有关系的数据对象却作为数据库的实例对象进行测试。非常糟糕的设计。
我要再次重申。 上面ORM问题导致的后果。 ORM基本缺点是撕裂了对象,可怕和明显违反了对象理念:一个对象是什么。