Hibernate 和 JPA 出了什么问题

jopen 10年前

对于关系型数据的持久化,Hibernate当然是市场上最流行的解决方案。它已有数年的历史了,而且有数以千计的项目用到了它。最新版本的Hibernate甚至遵循了Sun的Java持久化API(JPA)规范。所以,既然Hibernate可以完成所有事,为什么还要寻找其他方案呢?

我们认为Hibernate与JPA根本没有它们看起来得那么完美。我们将列举几个对Hibernate误解的原因来解释它为什么没有想像中的那么好。

数据模型定义:元数据的问题

所有关系型数据持久化解决方案都需要理解底层数据模型才能工作。Hibernate提供了两种定义数据模型的方式:XML映射文件和注解。注解是最近引入的以用于简化对象关系映射,而且注解比XML映射有很多优点。因此我们将不再考虑XML映射转而关注于注解。但是本文中与注解相关的所有内容都可以用相似的方式通过xml映射实现。

提供给Hibernate的元数据用于告知其保存对象到数据库中的位置和方式。这些信息可能不只用于Hibernate也要用于应用程序逻辑,所以你可能希望从代码中访问它而不是再次提供冗余的信息。一个适当的例子是某一文本域的长度,或者某个域是否是强制性的。例如在一个用户接口上,你可能需要这个信息用于展示一个表单输入控件或用于验证。

通过注解,你可以从代码中访问这个信息,但一个特别注解的属性不能像普通java对象的属性那样被直接引用。以下是一个示例:

这是使用注解访问元数据的方式:

Method field = Employee.class.getDeclaredMethod( "getFirstname", new Class[0] );  javax.persistence.Column col = field.getAnnotation( javax.persistence.Column.class );  int length = col.length();

这段代码与使用Empire-db的对象模型架构定义效果相同:

int length = mydb.EMPLOYEES.FIRSTNAME.getSize();

(注意EMPLOYEES和FIRSTNAME都是public final修饰的大写的成员变量,但也可以通过getter进行访问——具体访问方式取决于你的代码和决定)。

注解也有问题的原因不仅在于编译时安全的缺失与用户端代码的复杂性,它也有很多其他问题。持久化注解所提供的元数据通常并不充足。因此你需要像 Hibernate Validator 提供的附加的注解,或你可能想自定义注解,而这些会使你的映射与访问代码更加难以阅读和管理。更不用说在运行时改变你的数据模型是不可能的了。为元数据使用java对象模型要比以上这些方式都简单易行。既然不用注解可以做得更好那为什么还非要使用它呢?

我们认为注解不应该为应用逻辑提供可能用到的信息。注解应该只用于为代码提供编译器优化或代码文档这类具体信息。像@Deprecated 和@SuppressWarnings这类注解是可接受的。尽管注解较XML映射文件能更好地与java代码相结合,但它们的灵活性比普通的interface和类要差得很远。注解现在是很新酷的,但用得越广泛你的代码将会被污染得越严重。不要让该死的注解重蹈该死的XML配置的覆辙。

数据对象定义:泛滥的getter setter

除了元数据,我们也需要在某地存取我们自己的数据。对于Hibernate和JPA这个地方就是(装备了与相应表各字段对应的成员变量及它的getter setter方法的)JavaBean或POJO。对于大型数据模型这也就意味着很多行的代码。Hibernate工具可以通过逆向工程自动生成这些代码。但对于大型成熟的工程你可能会遇到这样的问题:一旦你手动更改了bean或映射代码——并且你希望保存这些改动——自动化工具就出问题了。所以通常这些代码(包括元数据)是通过手工维护的。更糟糕的是,因为这些对象通常用作填充业务对象的DTO(数据传送对象),你可能会看到用于Java对象间复制属性值的无数行代码。所以最好要把这些getter和setter放到哪里呢?

Empire-db的动态bean对于每个实现的实体都只有一个通用的getter和setter方法。我们仍推荐为每个数据库实体创建一个单独的数据对象类,这样类的总数没有变——虽然当使用一个通用DBRecord对象时这是没必要的, 但我们推荐这样做有两个原因:首先是为了类型安全,因为你希望你的代码依赖于特定的实体。其次,因为随着项目的增长,你很可能需要重写已经存在的方法并实现的新方法。尽管是这样,但因为少了这些成员变量和它们对应的getter setter方法,你会有相当少的代码需要维护。另外,如果有必要或为了简便,你可能要为某字段添加特殊的getter setter。

动态查询: 查询的困境

对于一个关系型数据库,我们希望它可以友好地支持动态查询,接下来我们看下Hibernate是如何处理动态查询的。 Hibernate提供两种方法: Hibernate查询语言(HQL)和标准的API。 HQL是Hibernate自己的语言,你必须先学习如何使用它。 它可以视为一个支持java编码映射的SQL方言。 当你尝试编译一个复杂的、带有约束和连接的、有条件查询语句时,就会发生问题,因为HQL是由不安全的字符串常量拼接成的。 我们认为在一些复杂度比较高的场景,使用HQL编写的代码会变得难以维护。 此时,标准的API是更好的选择,但是这种方法有缺点:灵活性比较低。

但这又出现了另一个问题:通常的编程任务是需要从一个或多个表中选出几个字段的集合或计算结果。查询结果可能用于展示给某一用户,或用于做其他处理。

对此Hibernate的HQL提供了可定义的select子句,原理是提供了一个特殊的结果bean,它持有你所需要的数据,甚至通过string拼接和数值运算这类SQL函数对其进行转换。但奇怪的这个功能在项目中几乎没有被用到过,人们转而使用全实体bean(full entity bean),这也就意味着从数据库中加载了很多没用的属性。对于实体间关系的解析,Hibernate既可以使用join(饿汉式)加载所有引用到的实体,也可以启用懒汉模式——查询每个引用到的对象——有时甚至只为了一个简单的属性。所以假设对于有5个属性的结果bean,实际上该对象不是持有5个属性,而是5个对象共加载了超过50个属性。很明显这不是你所期望的完美方案。

现在的问题是:如果人们没有使用正确的方法,到底是人的错还是工具的错呢?为了找到答案,我问过我自己:如果我在使用Hibernate,我会一直为我的只读查询使用结果bean吗?还是使用全实体bean?

从性能上看我当然要使用结果bean了。但从代码质量来说我可能不会这样做。毕竟像这种表达式

filter.append("select new EmployeeResult(employee.employeeId,                  employee.firstname || ', ' || employee.lastname,                  employee.gender, employee.dateOfBirth,                  department.name) ");

根本就不能提高我的代码质量。除了避免拼写错误,结果bean的构造函数参数也必须要与这段代码相匹配。因此错误只能在运行时被发现,从而使所有错误变得更加恶心。并且毕竟——我必须承认——我有时懒得敲这些属性名,更不用说它们还带着前缀呢。

最后我可能会根据实体的大小和查询的性质来使用这两种方法。但我能理解为什么人们要避开这个高效合理的特性——却愿意付出高内存消耗与低性能的代价。

而Empire-db却简单得多。你只需要明确地转换和选择你在结果集中需要的字段,因为这样花费的代价是较低的。首先你要列出查询的所有字段,甚至包括转换函数(通过IDE的代码补全功能会容易点)。然后你可以通过构造函数或setter将它们自动或手动保存到一个JavaBean中。最重要的是你可以完全无字符串地实现且它是100%编译时安全的。以下是一个示例:

要构建的最终 SQL (Oracle syntax):

SELECT t2.EMPLOYEE_ID, t2.LASTNAME || ', ' || t2.FIRSTNAME AS NAME,         t1.NAME AS DEPARTMENT  FROM (DEPARTMENTS t1 INNER JOIN EMPLOYEES t2         ON t2.DEPARTMENT_ID = t1.DEPARTMENT_ID)  WHERE upper(t2.LASTNAME) LIKE upper('Foo%') AND t2.RETIRED=0  ORDER BY t2.LASTNAME, t2.FIRSTNAME

使用Empire-db:

SampleDB db = getDatabase();  // Declare shortcuts (not necessary but convenient)  SampleDB.Employees EMP = db.EMPLOYEES;  SampleDB.Departments DEP = db.DEPARTMENTS;  // Create a command object  DBCommand cmd = db.createCommand();  // Select columns  cmd.select(EMP.EMPLOYEE_ID);  cmd.select(EMP.LASTNAME.append(", ").append(EMP.FIRSTNAME).as("NAME"));  cmd.select(DEP.NAME.as("DEPARTMENT"));  // Join tables  cmd.join  (DEP.DEPARTMENT_ID, EMP.DEPARTMENT_ID);  // Set constraints  cmd.where(EMP.LASTNAME.likeUpper("Foo%"));  cmd.where(EMP.RETIRED.is(false));  // Set order  cmd.orderBy(EMP.LASTNAME);  cmd.orderBy(EMP.FIRSTNAME);


我们的结论

事实上,Hibernate是最高级的传统ORM解决方案之一。不管怎样,使用ORM的问题在于,它的设计初衷主要是依赖整个实体才能工作。另一方面,关系型数据库提供了强大的组合、过滤和转换实体及它们的字段的能力。为了保留这些灵活性,Hibernate提供了各种特性来弥补这些差距。但是为了正确的使用这些特性,你必须做很多抉择: XML还是注解,HQL还是criteria API,懒装还是即使获取等等。Hibernate内部所做的工作,尤其是当它实际执行一次数据库连接时所做的事情并不总是简单明了的(如果你把日志级别调到debug - 你就能看到它的复杂性)。当使用和配置恰当的时候,Hibernate也许能工作的很好,但是这需要花费很长时间去了解很多注意事项。尤其是,如果你对Hibernate还不熟悉,那么学习曲线还是很陡峭的。

Hibernate 最缺少的是对编译期安全性的支持。在使用HQL和criteria API时,你都需要提供字符串化的属性名称甚至整个SQL语句 – 这使得每次数据模型的变更都成为一次冒险 –  这个问题只能通过外部的并且是费时的测试来解决。

相反的,Empire-db解决编译期安全性问题的方法是,基于Java对象模型定义提供一套类型安全的API。任何时候你修改了你的模型定义,你的Java编译器就会明确的告诉你这次修改影响了哪些代码。这极大的提高了代码质量,并减少了测试的数量和耗时。附带的好处是你的编码效率也会提高,因为在你构建动态查询时,你的IDE将会允许你浏览所有表、字段甚至SQL函数。

Empire-db is not an ORM solution as you know it. Its focus is clearly on modelling the way relational databases  work in Java and not vice versa. Empire-db is passive and does not interfere with your connection and transaction  handling – making it easy to integrate and requiring zero configuration. It does not automatically resolve object  references but since you select the data exactly as you need it, there is rarely demand for this. Still if you  need it, you may simply add a getter with a few simple lines of code. In this case – we believe – that less  sometimes is more.

We recommend that if you are not very familiar with SQL and all  you need is to store away and reload your POJO's, Hibernate or another  JPA implementation is probably the  better choice. But if you want to get the most out of  SQL and you want to keep full control over when which statements are  executed, with all the additional benefits  of metadata access and compile-time safety then you really should  give Empire-db a go.

Note: If you feel that any of the criticism we made about Hibernate is without reason please let us know. E-mail: