本文由码农网 – 孙腾浩原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划

(“Too Long; Didn’t Read.”太长不想看,可以看这段摘要 )ORM是一种讨厌的反模式,违背了所有面向对象的原则。将对象分隔开并放入被动的数据包中。ORM没有理由存在任何应用中,无论是小的网络应用还是涉及增删改查上千张表的企业系统。用什么来替代它呢?SQL对象(SQL-speaking objects)。

ORM如何工作

Object-relational mapping(ORM)是一种链接关系型数据库和面向对象语言(比如Java)的技术。在各种语言中有大量ORM的实现;比如:Java中的Hibernate,Ruby on Rails中的ActiveRecord,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:create, read, update, and delete).首先,我们创建一个 Post 类(很抱歉代码很长,我尽量简洁一点)

  1. @Entity
  2. @Table(name = "post")
  3. public class Post {
  4. private int id;
  5. private Date date;
  6. private String title;
  7. @Id
  8. @GeneratedValue
  9. public int getId() {
  10. return this.id;
  11. }
  12. @Temporal(TemporalType.TIMESTAMP)
  13. public Date getDate() {
  14. return this.date;
  15. }
  16. public Title getTitle() {
  17. return this.title;
  18. }
  19. public void setDate(Date when) {
  20. this.date = when;
  21. }
  22. public void setTitle(String txt) {
  23. this.title = txt;
  24. }
  25. }

在Hibernate做任何操作前,我们要创建一个session工厂:

  1. SessionFactory factory = new AnnotationConfiguration()
  2. .configure()
  3. .addAnnotatedClass(Post.class)
  4. .buildSessionFactory();

这个工厂能在每次我们需要操作 Post对象时给我们”session”。任何有关session的操作应该包裹下面代码块:

  1. Session session = factory.openSession();
  2. try {
  3. Transaction txn = session.beginTransaction();
  4. // your manipulations with the ORM, see below
  5. txn.commit();
  6. } catch (HibernateException ex) {
  7. txn.rollback();
  8. } finally {
  9. session.close();
  10. }

当session准备就绪,下面是我们从数据库表中获取所有 post:

  1. List posts = session.createQuery("FROM Post").list();
  2. for (Post post : (List<Post>) posts){
  3. System.out.println("Title: " + post.getTitle());
  4. }

我认为到这就很简单了。Hibernate是一个强大的引擎,链接数据库,执行SQL SELECT请求,然后取得数据。然后实例化 Post对象并装填数据。当我们得到对象时,它已经有了数据,比如我们上面使用 getTitle()。

当我们想进行反向操作,将一个对象存入数据库,操作相同,顺序相反。我们先实例化 Post对象,装填数据,然后让Hibernate保存它:

  1. Post post = new Post();
  2. post.setDate(new Date());
  3. post.setTitle("How to cook an omelette");
  4. session.save(post);

这就是所有的ORM如何工作。基本原则总是一样的——ORM对象就是数据的信使。我们与ORM框架交互。框架与数据库交互。对象只是帮助我们向框架发送请求并处理响应。除了get和set,没有其他方法,我们甚至不知道数据库在哪。

这就是对象-关系映射如何工作。

你或许会问哪里有问题?到处!

ORM有什么问题?

讲真的,这有什么问题?Hibernate成为最受欢迎的Java库已有10多年,几乎所有处理SQL的应用都在使用它,每个Java教程都会介绍Hibernate(或其他ORM,比如TopLink和OpenJPA)作为数据库连接应用,它已成为一个标准。我还要认为它有问题吗?当然。

我想说整个ORM的构想就是有问题的,它的发明简直是面向对象编程里NULL指针之后的第二大错误。

事实上,我并不是第一个指出这个问题的人,有众多知名作者讨论这个话题,包括Martin Fowler写的OrmHate(虽然不是反对ORM,但是也值得关注),Jeff Atwood写的Object-Relational Mapping Is the Vietnam of Computer Science,Ted Neward写的The Vietnam of Computer Science,Laurie Voss写的ORM Is an Anti-Pattern等等。

然而,我的论点不同于上面几位,尽管他们的论点实用又有根据,比如“ORM很慢”或“数据库升级困难”,他们没抓住重点。你可以在Bozhidar Bozhanov的ORM Haters Don’t Get It这篇博客中找到很棒的论点。

重点是ORM没有在对象中封装数据库的交互,而是把固定的数据和活动的交互分隔开。一部分在对象中储存数据,另一部分由ORM引擎实现(session工厂)来与数据库交互数据。上面这张图展示了ORM做了什么。

我作为 post数据的操作者,需要处理两个组件:一是ORM,二是返回的”删减版”(指只有get和set方法)对象。面向对象编程强调的是关注单一的切入点,也就是对象。但在ORM中,我需要关注两个切入点——ORM引擎和我们甚至不能称之为对象的”东西”。

因为这严重违背了面向对象的范式,我们可以从很多德高望重的论文中找到实用的解决方案,我可以再提供更多的解决方案。

SQL不再隐藏(SQL Is Not Hidden),ORM用户常使用SQL(或其他方言,比如HQL)。上面例子中,我们调用 session.createQuery(“FROM Post”) 来获取所有 post。即使这不是SQL,但很像SQL,所以关系模型并没有封装在对象中。反而暴露在整个应用中,每个人操作对象时无可避免的需要处理关系模型,来获取或存储数据。所以ORM并没有隐藏和包裹SQL,反而使其暴露在整个应用中。

难于测试。如果某个对象处理post的数组,它必须处理 SessionFactory的实例。我们如何mock这个依赖呢?看上面的代码,你就会意识到单元测试会有多繁琐和麻烦。相反,我们可以编写集成测试,用整个应用链接到虚构的测试PostgreSQL。这样,我们就不需要mock一个 SessionFactory,但这样的测试会很慢,更重要的是,我们虚构的和数据库无关这个对象会和数据库实例冲突。这是个可怕的设计。

我再次重申,ORM的实际问题就是这种后果,本质缺点就是ORM将对象分隔开,严重违反了对象的含义。

SQL对象(SQL-Speaking Objects)

有什么解决方案?让我给你举个例子,我们来设计一个 Post类,我们需要将它分成两个类:Post和 Posts,单个和多个。我曾在我的一篇文章中提到过,一个好的对象是现实生活中实体的抽象,我们来实践这一原则。我们有两个实体:数据库表和表格行,这就是为什么我们创建两个类:Post展示表,Post展示行。

我也曾在文章中说过,每个对象应该关联并实现一个接口,让我们先来创建两个接口,当然我们的对象是不可变的,Posts应该是这样的:

  1. interface Posts {
  2. Iterable<Post> iterate();
  3. Post add(Date date, String title);
  4. }

单一的 Post应该是这样的:

  1. interface Post {
  2. int id();
  3. Date date();
  4. String title();
  5. }

遍历数据库中的所有的post:

  1. Posts posts = // we'll discuss this right now
  2. for (Post post : posts.iterate()){
  3. System.out.println("Title: " + post.title());
  4. }

创建一个新post:

  1. Posts posts = // we'll discuss this right now
  2. posts.add(new Date(), "How to cook an omelette");

你可以看到,我们有真实对象了,他们掌握所有操作,并且在内部隐藏实现细节,没有事务,会话或工厂,我们甚至不知道这些对象是否真的和PostgreSQL交互,或许它只是把数据保存在txt文件中。Posts带给我们的是获取post列表和创建新post的能力,具体实现很好地隐藏在其中,现在让我们看一看如何实现这两个类。

我将使用jcabi-jdbc作为JDBC包裹,当然你也可以使用其他你喜欢的JDBC,这无所谓,重点是与数据库的交互要隐藏在对象中,让我们开始实现 PgPosts类(“pg”表示PostgreSQL)。

  1. final class PgPosts implements Posts {
  2. private final Source dbase;
  3. public PgPosts(DataSource data) {
  4. this.dbase = data;
  5. }
  6. public Iterable<Post> iterate() {
  7. return new JdbcSession(this.dbase)
  8. .sql("SELECT id FROM post")
  9. .select(
  10. new ListOutcome<Post>(
  11. new ListOutcome.Mapping<Post>() {
  12. @Override
  13. public Post map(final ResultSet rset) {
  14. return new PgPost(
  15. this.dbase,
  16. rset.getInteger(1)
  17. );
  18. }
  19. }
  20. )
  21. );
  22. }
  23. public Post add(Date date, String title) {
  24. return new PgPost(
  25. this.dbase,
  26. new JdbcSession(this.dbase)
  27. .sql("INSERT INTO post (date, title) VALUES (?, ?)")
  28. .set(new Utc(date))
  29. .set(title)
  30. .insert(new SingleOutcome<Integer>(Integer.class))
  31. );
  32. }
  33. }

然后我们创建 PgPost类来实现 Post接口:

  1. final class PgPost implements Post {
  2. private final Source dbase;
  3. private final int number;
  4. public PgPost(DataSource data, int id) {
  5. this.dbase = data;
  6. this.number = id;
  7. }
  8. public int id() {
  9. return this.number;
  10. }
  11. public Date date() {
  12. return new JdbcSession(this.dbase)
  13. .sql("SELECT date FROM post WHERE id = ?")
  14. .set(this.number)
  15. .select(new SingleOutcome<Utc>(Utc.class));
  16. }
  17. public String title() {
  18. return new JdbcSession(this.dbase)
  19. .sql("SELECT title FROM post WHERE id = ?")
  20. .set(this.number)
  21. .select(new SingleOutcome<String>(String.class));
  22. }
  23. }

下面就是我们用刚刚创建的类来和数据库交互:

  1. Posts posts = new PgPosts(dbase);
  2. for (Post post : posts.iterate()){
  3. System.out.println("Title: " + post.title());
  4. }
  5. Post post = posts.add(new Date(), "How to cook an omelette");
  6. System.out.println("Just added post #" + post.id());

你可以在这看到完整例子.这是一个开源的web app使用PostgreSQL来实现上面提到的-SQL-speaking objects.

性能如何?

我能听到你的惊呼“性能怎么样?”在上面几行代码中,我们创建了和数据库的冗余链接。首先我们用 SELECT id来检索post的ID,然后为了获取它们的title,我们对应每个post发送 SELECT title请求,这确实效率很低。

不要担心,这是面向对象编程,意味着这是可伸缩的!我们来创建一个 PgPost的装饰器,其构造函数接受数据,并在内部缓存:

  1. final class ConstPost implements Post {
  2. private final Post origin;
  3. private final Date dte;
  4. private final String ttl;
  5. public ConstPost(Post post, Date date, String title) {
  6. this.origin = post;
  7. this.dte = date;
  8. this.ttl = title;
  9. }
  10. public int id() {
  11. return this.origin.id();
  12. }
  13. public Date date() {
  14. return this.dte;
  15. }
  16. public String title() {
  17. return this.ttl;
  18. }
  19. }

注意:这个装饰器并不知道PostgreSQL或JDBC,它仅仅是 POST对象的装饰,并缓存数据和title。通常,这个装饰器是不可变的。

现在我们来创建 Posts的另一个实现,其返回一个”不可变”的对象:

  1. final class ConstPgPosts implements Posts {
  2. // ...
  3. public Iterable<Post> iterate() {
  4. return new JdbcSession(this.dbase)
  5. .sql("SELECT * FROM post")
  6. .select(
  7. new ListOutcome<Post>(
  8. new ListOutcome.Mapping<Post>() {
  9. @Override
  10. public Post map(final ResultSet rset) {
  11. return new ConstPost(
  12. new PgPost(
  13. ConstPgPosts.this.dbase,
  14. rset.getInteger(1)
  15. ),
  16. Utc.getTimestamp(rset, 2),
  17. rset.getString(3)
  18. );
  19. }
  20. }
  21. )
  22. );
  23. }
  24. }

现在所有post通过这个 iterate()方法返回,并且从数据库中取到了数据装配在新的类中。

使用装饰器和对相同接口的众多实现,你可以组合任意的功能,最重要的是扩展功能的同时,不要增加设计的复杂度,因为类的大小不会增长,我们使用新的高聚合的类,它们更小巧。

事务又如何?

每个对象应该在其单独的事务中执行,并且将 SELECT和 INSERT封装在一起,这需要内置事务,数据库提供非常棒的支持。如果没有这样的支持,创建一个会话事务对象必须接受一个”callable”类,比如:

  1. final class Txn {
  2. private final DataSource dbase;
  3. public <T> T call(Callable<T> callable) {
  4. JdbcSession session = new JdbcSession(this.dbase);
  5. try {
  6. session.sql("START TRANSACTION").exec();
  7. T result = callable.call();
  8. session.sql("COMMIT").exec();
  9. return result;
  10. } catch (Exception ex) {
  11. session.sql("ROLLBACK").exec();
  12. throw ex;
  13. }
  14. }
  15. }

然后,当你想在一个事务中进行一系列的操作,像下面这样:

  1. new Txn(dbase).call(
  2. new Callable<Integer>() {
  3. @Override
  4. public Integer call() {
  5. Posts posts = new PgPosts(dbase);
  6. Post post = posts.add(new Date(), "How to cook an omelette");
  7. posts.comments().post("This is my first comment!");
  8. return post.id();
  9. }
  10. }
  11. );

这段代码会创建一个新的post并提交一个comment.如果任何调用失败,整个事务将回滚。

对于我来说,这就是面向对象,我称它为”SQL-speaking objects”,因为它们知道如何与数据库服务器通过SQL交互,这是它们的能力,完美封装在其内部。

ORM 是一种讨厌的反模式的更多相关文章

  1. 《SQL 反模式》 学习笔记

    第一章 引言 GoF 所著的的<设计模式>,在软件领域引入了"设计模式"(design pattern)的概念. 而后,Andrew Koenig 在 1995 年造了 ...

  2. [转]为什么我说ORM是一种反模式

    原文地址:http://www.nowamagic.net/librarys/veda/detail/2217 上周我在在上讨论了ORM,在那以后有人希望我澄清我的意思.事实上,我曾经写文章讨论过OR ...

  3. SQL反模式学习笔记21 SQL注入

    目标:编写SQL动态查询,防止SQL注入 通常所说的“SQL动态查询”是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句. 反模式:将未经验证的输入作为代码执行 当向SQL查询的字符串中插入别 ...

  4. Python编程中的反模式

    Python是时下最热门的编程语言之一了.简洁而富有表达力的语法,两三行代码往往就能解决十来行C代码才能解决的问题:丰富的标准库和第三方库,大大节约了开发时间,使它成为那些对性能没有严苛要求的开发任务 ...

  5. Apache Hadoop最佳实践和反模式

    摘要:本文介绍了在Apache Hadoop上运行应用程序的最佳实践,实际上,我们引入了网格模式(Grid Pattern)的概念,它和设计模式类似,它代表运行在网格(Grid)上的应用程序的可复用解 ...

  6. 开发反模式 - SQL注入

    一.目标:编写SQL动态查询 SQL常常和程序代码一起使用.我们通常所说的SQL动态查询,是指将程序中的变量和基本SQL语句拼接成一个完整的查询语句. string sql = SELECT * FR ...

  7. 开发反模式(GUID) - 伪键洁癖

    一.目标:整理数据 有的人有强迫症,他们会为一系列数据的断档而抓狂. 一方面,Id为3这一行确实发生过一些事情,为什么这个查询不返回Id为3的这一行?这条记录数据丢失了吗?那个Column到底是什么? ...

  8. SQL反模式学习笔记1 开篇

    什么是“反模式” 反模式是一种试图解决问题的方法,但通常会同时引发别的问题. 反模式分类 (1)逻辑数据库设计反模式 在开始编码之前,需要决定数据库中存储什么信息以及最佳的数据组织方式和内在关联方式. ...

  9. SQL反模式学习笔记5 外键约束【不用钥匙的入口】

    目标:简化数据库架构 一些开发人员不推荐使用引用完整性约束,可能不使用外键的原因有一下几点: 1.数据更新有可能和约束冲突: 2.当前的数据库设计如此灵活,以至于不支持引用完整性约束: 3.数据库为外 ...

随机推荐

  1. 富有魅力的git stash

    git stash 会把当前的改动暂时搁置起来, 也就是所谓的git 暂存区. 你可以执行 git stash list 来查看你所有暂存的东东. 也可以 git stash apple ** 来拿下 ...

  2. [HDOJ1827]Summer Holiday(强连通分量,缩点)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1827 缩点后统计入度和当前强连通分量中最小花费,然后记录入度为0的点的个数和花费和就行了. /* ━━ ...

  3. git cheat sheet,git四张手册图

  4. MSSQL中把表中的数据导出成Insert

    use master go if exists (select name from sysobjects where name = 'sp_generate_insert_script') begin ...

  5. 发布 windows 10 universal app 时微软账号验证失败

    具体错误:Visual Studio encountered an unexpected network error and can't contact the Microsoft account s ...

  6. POJ 3080 (字符串水题) Blue Jeans

    题意: 找出这些串中最长的公共子串(长度≥3),如果长度相同输出字典序最小的那个. 分析: 用库函数strstr直接查找就好了,用KMP反而是杀鸡用牛刀. #include <cstdio> ...

  7. 面向函数范式编程(Functional programming)

    函数编程(简称FP)不只代指Haskell Scala等之类的语言,还表示一种编程思维,软件思考方式,也称面向函数编程. 编程的本质是组合,组合的本质是范畴Category,而范畴是函数的组合. 首先 ...

  8. Android SharedPreferences 权限设置

    说明: 由于目前打算采用两个app来完成一件事,采用SharedPreferences来做数据交换,于是突然想验证一下Java层的权限设置会不会就是设置Linux下文件的权限,验证的结果是这样的. T ...

  9. Swift入门篇-基本类型(1)

    博主语文一直都不好(如有什么错别字,请您在下评论)望您谅解,没有上过什么学的 今天遇到了一个很烦的事情是,早上10点钟打开电脑,一直都进入系统(我的系统  mac OS X Yosemite 10.1 ...

  10. 搭建LAMP测试环境

    LAMP:Linux+Apache+Mysql+Php,组合统称为LAMP,关于其中的独立个体,这里就不多介绍了. 1.首先准备一下软件包,如下: mysql-5.0.22.tar.gz httpd- ...