原文: https://segmentfault.com/a/1190000018472572

说明

因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ 是总体上最好的,可惜不是完全免费,最终选择 JDBC Template。

Hibernate 和 Mybatis 是使用最多的两个主流框架,而 JOOQ、Ebean 等小众框架则知道的人不多,但也有很多独特的优点;而 JPA 则是一组 Java 持久层 Api 的规范,Spring Data JPA 是 JPA Repository 的实现,本来和 Hibernate、Mybatis、JOOQ 之类的框架不在同一个层次上,但引入 Spring Data JPA 之类框架之后,我们会直接使用 JPA 的 API 查询更新数据库,就像我们使用 Mybatis 一样,所以这里也把 JPA 和其他框架放在一起进行比较。

同样,JDBC 和其他框架也在同一层次,位于所有持久框架的底层,但我们有时候也会直接在项目中使用 JDBC,而 Spring JDBC Template 部分消除了使用 JDBC 的繁琐细节,降低了使用成本,使得我们更加愿意在项目中直接使用 JDBC。

一、SQL 封装和性能

在使用 Hibernate 的时候,我们查询的是 POJO 实体类,而不再是数据库的表,例如 hql 语句 select count(*) from User,里面的 User 是一个 Java 类,而不是数据库表 User。这符合 ORM 最初的理想,ORM 认为 Java 程序员使用 OO 的思维方式,和关系数据库的思维方式差距巨大,为了填补对象和关系思维方式的鸿沟,必须做一个对象到关系的映射,然后在 Java 的对象世界中,程序员可以使用纯的对象的思维方式,查询 POJO 对象,查询条件是对象属性,不再需要有任何表、字段等关系的概念,这样 java 程序员就更容易做持久层的操作。

JPA 可以视为 Hibernate 的儿子,也继承了这个思路,把 SQL 彻底封装起来,让 Java 程序员看不到关系的概念,用纯的面向对象思想,重新创造一个新的查询语言代替 sql,比如 hql,还有 JPQL 等。支持 JPA 的框架,例如 Ebean 都属于这种类型的框架。

但封装 SQL,使用另一种纯的面向对象查询语言代替 sql,真的能够让程序员更容易实现持久层操作吗?MyBatis 的流行证明了事实并非如此,至少在大多数情况下,使用 hql 并不比使用 sql 简单。首先,从很多角度上看,hql/JPQL 等语言更加复杂和难以理解;其次就是性能上明显降低,速度更慢,内存占用巨大,而且还不好优化。最为恼火的是,当关系的概念被替换为对象的概念之后,查询语言的灵活性变得很差,表达能力也比 sql 弱很多。写查询语句的时候受到各种各样的限制,一个典型的例子就是多表关联查询。

不管是 hibernate 还是 jpa,表之间的连接查询,被映射为实体类之间的关联关系,这样,如果两个实体类之间没有(实现)关联关系,你就不能把两个实体(或者表)join 起来查询。这是很恼火的事情,因为我们很多时候并不需要显式定义两个实体类之间的关联关系就可以实现业务逻辑,如果使用 hql,只是为了 join 我们就必须在两个实体类之间添加代码,而且还不能逆向工程,如果表里面没有定义外键约束的话,逆向工程会把我们添加的关联代码抹掉。

MyBatis 则是另外一种类型的持久化框架,它没有封装 SQL 也没有创建一种新的面相对象的查询语言,而是直接使用 SQL 作为查询语言,只是把结果填入 POJO 对象而已。使用 sql 并不比 hql 和 JPQL 困难,查询速度快,可以灵活使用任意复杂的查询只要数据库支持。从 SQL 封装角度上看,MyBatis 比 Hibernate 和 JPA 成功,SQL 本不该被封装和隐藏,让 Java 程序员使用 SQL 既不麻烦也更容易学习和上手,这应该是 MyBatis 流行起来的重要原因。

轻量级持久层框架 JOOQ 也和 MyBatis 一样,直接使用 SQL 作为查询语言,比起 MyBatis,JOOQ 虽然知名度要低得多,但 JOOQ 不但和 MyBatis 一样可以利用 SQL 的灵活性和高效率,通过逆向工程,JOOQ 还可以用 Java 代码来编写 SQL 语句,利用 IDE 的代码自动补全功能,自动提示表名和字段名,减少程序员记忆负担,还可以在元数据发生变化时发生编译错误,提示程序员修改相应的 SQL 语句。

Ebean 作为一种基于 JPA 的框架,它也使用 JPQL 语言进行查询,多数情况下会让人很恼火。但据说 Ebean 不排斥 SQL,可以直接用 SQL 查询,也可以用类似 JOOQ 的 DSL 方式在代码中构造 SQL 语句(还是 JPQL 语句?),但没用过 Ebean,所以具体细节不清楚。

JDBC Template 就不用说了,它根本没做 ORM,当然是纯 SQL 查询。利用 Spring 框架,可以把 JDBC Template 和 JPA 结合起来使用,在 JPA 不好查询的地方,或者效率低不好优化的地方使用 JDBC,缓解了 Hibernate/JPA 封装 SQL 造成的麻烦,但我仍没看到任何封装 SQL 的必要性,除了给程序员带来一大堆麻烦和学习负担之外,没有太明显的好处。

二、DSL 和变化适应性

为了实现复杂的业务逻辑,不论是用 SQL 还是 hql 或者 JPQL,我们都不得不写很多简单的或者复杂的查询语句,ORM 无法减少这部分工作,最多是用另一种面向对象风格的语言去表达查询需求,如前所述,用面向对象风格的语言不见得比 SQL 更容易。通常业务系统中会有很多表,每个表都有很多字段,即便是编写最简单的查询语句也不是一件容易的事情,需要记住数据库中有哪些表,有哪些字段,记住有哪些函数等。写查询语句很多时候成为一件头疼的事情。

QueryDSL、JOOQ、Ebean 甚至 MyBatis 和 JPA 都设计一些特性,帮助开发人员编写查询语句,有人称之为 “DSL 风格数据库编程”。最早实现这类功能的可能是 QueryDSL,把数据库的表结构逆向工程为 java 的类,然后可以让 java 程序员能够用 java 的语法构造出一个复杂的查询语句,利用 IDE 的代码自动补全功能,可以自动提示表名、字段名、查询语句的关键字等,很成功的简化了查询语句的编写,免除了程序员记忆各种名字、函数和关键字的负担。

QueryDSL 有很多版本,但用得多的是 QueryDSL JPA,可以帮助开发人员编写 JPQL 语句,如前所述,JPQL 语句有很多局限不如 SQL 灵活高效。后来的 JOOQ 和 Ebean,基本上继承了 QueryDSL 的思路,Ebean 基本上还是 JPA 风格的 ORM 框架,虽然也支持 SQL,但不清楚其 DSL 特性是否支持 SQL 语句编写,在官网上看到的例子都是用于构造 JPQL 语句。

这里面最成功的应该是 JOOQ,和 QueryDSL 不同,JOOQ 的 DSL 编程是帮助开发人员编写 SQL 语句,抛弃累赘的 ORM 概念,JOOQ 这个功能非常轻小,非常容易学习和使用,同时性能也非常好,不像 QueryDSL 和 Ebean,需要了解复杂的 JPA 概念和各种奇异的限制,JOOQ 编写的就是普通的 SQL 语句,只是把查询结果填充到实体类中(严格说 JOOQ 没有实体类,只是自动生成的 Record 对象),JOOQ 甚至不一定要把结果转换为实体类,可以让开发人员按照字段取得结果的值,相对于 JDBC,JOOQ 会把结果值转换为合适的 Java 类型,用起来比 JDBC 更简单。

传统主流的框架对 DSL 风格支持得很少,Hibernate 里面基本上没有看到有这方面的特性。MyBatis 提供了 “SQL 语句构建器” 来帮助开发人员构造 SQL 语句,但和 QueryDSL/JOOQ/Ebean 差很多,不能提示表名和字段名,语法也显得累赘不像 SQL。

JPA 给人的印象是复杂难懂,它的 MetaModel Api 继承了特点,MetaModel API+Criteria API,再配合 Hibernate JPA 2 Metamodel Generator,让人有点 QueryDSL JPA 的感觉,只是绕了一个大大的弯,叠加了好几层技术,最后勉强实现了 QueryDSL JPA 的简单易懂的功能。很多人不推荐 JPA+QueryDSL 的用法,而是推荐 JPA MetaModel API+Criteria API+Hibernate JPA 2 Metamodel Generator 的用法,让人很难理解,也许是因为这个方案是纯的标准的 JPA 方案。

数据库 DSL 编程的另一个主要卖点是变化适应性强,数据库表结构在开发过程中通常会频繁发生变化,传统的非 DSL 编程,字段名只是一个字符串,如果字段名或者类型改变之后,查询语句没有相应修改,编译不会出错,也容易被开发人员忽略,是 bug 的一个主要来源。DSL 编程里面,字段被逆向工程为一个 java 类的属性,数据库结构改变之后,作为 java 代码一部分的查询语句会发生编译错误,提示开发人员进行修改,可以减少大量 bug,减轻测试的负担,提高软件的可靠性和质量。

三、跨数据库移植

Hibernate 和 JPA 使用 hql 和 JPQL 这类数据库无关的中间语言描述查询,可以在不同数据库中无缝移植,移植到一个 SQL 有巨大差别的数据库通常不需要修改代码或者只需要修改很少的代码。Ebean 如果不使用原生 SQL,而是使用 JPA 的方式开发,也能在不同数据库中平滑的移植。

MyBatis 和 JOOQ 直接使用 SQL,跨数据库移植时都难免要修改 SQL 语句。这方面 MyBatis 比较差,只有一个动态 SQL 提供的特性,对于不同的数据库编写不同的 sql 语句。

JOOQ 虽然无法像 Hibernate 和 JPA 那样无缝移植,但比 MyBatis 好很多。JOOQ 的 DSL 很大一部分是通用的,例如分页查询中,Mysql 的 limit/offset 关键字是很方便的描述方式,但 Oracle 和 SQLServer 的 SQL 不支持,如果我们用 JOOQ 的 DSL 的 limit 和 offset 方法构造 SQL 语句,不修改移植到不支持 limit/offset 的 Oracle 和 SQLServer 上,我们会发现这些语句还能正常使用,因为 JOOQ 会把 limit/offset 转换成等价的目标数据库的 SQL 语句。JOOQ 根据目标数据库转换 SQL 语句的特性,使得在不同数据库之间移植的时候,只需要修改很少的代码,明显优于 MyBatis。

JDBC Template 应该最差,只能尽量使用标准 sql 语句来减少移植工作量。

四、安全性

一般来说,拼接查询语句都会有安全隐患,容易被 sql 注入攻击。不论是 jdbc,还是 hql/JPQL,只要使用拼接的查询语句都是不安全的。对于 JDBC 来说,使用参数化的 sql 语句代替拼接,可以解决问题。而 JPA 则应该使用 Criteria API 解决这个问题。

对于 JOOQ 之类的 DSL 风格框架,最终会被 render 为参数化的 sql,天生免疫 sql 注入攻击。Ebean 也支持 DSL 方式编程,也同样免疫 sql 注入攻击。

这是因为 DSL 风格编程参数化查询比拼接字符串查询更简单,没人会拼接字符串。而 jdbc/hql/JPQL 拼接字符串有时候比参数化查询更简单,特别是 jdbc,很多人会偷懒使用不安全的方式。

五、JOOQ 的失败之处

可能大部分人会不同意,虽然 Hibernate、JPA 仍然大行其道,是最主流的持久化框架,但其实这种封装 SQL 的纯正 ORM 已经过时,效益低于使用它们的代价,应该淘汰了。MyBatis 虽然有很多优点,但它的优点 JOOQ 基本上都有,而且多数还更好。MyBatis 最大的缺点是难以避免写 xml 文件,xml 文件编写困难,容易出错,还不容易查找错误。相对于 JOOQ,MyBatis 在多数情况下没有任何优势。

Ebean 同时具有很多不同框架的优点,但它是基于 JPA 的,难免有 JPA 的各种限制,这是致命的缺点。

JOOQ 这个极端轻量级的框架技术上是最完美的,突然有一天几个 Web 系统同时崩了,最后发现是 JOOQ 试用期过期了,这是 JOOQ 的失败之处,它不是完全免费的,只是对 MySql 之类的开源数据库免费。

最终,我决定选择 JDBC Template。