本文从这个 Bug 开始,讲述了如何启用新 TableGenerator,并演示了新 TableGenerator 的完整用法。
问题的发现源自对 JPA 中 TableGenerator 的测试。测试的环境有这样几个条件:
为方便查询的测试,Employee 表格在初始化时会导入部分记录,这部分记录的主键在初始脚本中手动写好,比如 1、2、3、4。(参看文章所附示例代码中的 import_data.sql 文件)。 Employee 实体使用 TableGenerator 主键生成器,initialValue 的值设置为 10。 在单元测试中添加新的 Employee 记录。
Employee 实体类的代码参看清单 1:
清单 1. Employee 实体类
@Entity @Table(">name="emp3") public class Employee3 { @TableGenerator(name="id_gen",table="id_gen",initialValue=10) @Id @GeneratedValue(strategy=TABLE,generator="id_gen") private long id; private String firstName; private String lastName; ...... }
@TableGenerator 的配置参数 initialValue 指的是主键生成列的初始值,这在 @TableGenerator 的 API 文档中写得很清楚。现在 initialValue 值设置为 10, 那么在单元测试中用 JPA 添加新的 Employee 记录时,新记录的主键会从 11 开始,不会与已有的数据发生冲突(参看文章所附示例代码中 src/java/test/sample/case3/OldInitialValue.java)。执行的结果出乎意料,测试报错,说是主键重复。错误信息如清单 2 所示:
清单 2. 主键重复错误信息
11:23:40,220 ERROR SqlExceptionHelper:144 - Duplicate entry '1' for key 'PRIMARY'
这实在令人困惑。如果 initialValue 的含义不是初始值,那还能是什么呢?
问题其实出在程序所用的 JPA 提供者(Hibernate)上面。如果改用其他 JPA 提供者,估计不会出现上面的问题(未验证)。Hibernate 之所以会出现这种情况,并非无知,也不是不尊重标准,而有它自身的原因,这在文章后面会提到。现在,为了把问题讲清楚, 有必要先谈谈 JPA 主键生成器选型的问题,了解一下 @TableGenerator 在 JPA 中的特殊地位。
JPA 主键生成器选型
JPA 提供了四种主键生成器,参看表 1:
表 1. JPA 的四种主键生成器
生成器名称 描述 AUTO 由 JPA 提供者根据数据库自行决定生成算法。 IDENTITY 由数据库的自增列提供主键值。 SEQUENCE 由数据库 Sequence 对象提供主键值。 TABLE 由 JPA 提供者通过创建数据库表来记录生成的主键值。
一般来说,支持 IDENTITY 的数据库,如 MySQL、SQL Server、DB2 等,AUTO 的效果与 IDENTITY 相同。IDENTITY 主键生成器最大的特点是:在表中插入记录以后主键才会生成。这意味着,实体对象只有在保存到数据库以后,才能得到主键值。用 EntityManager 的 persist 方法来保存实体时必须在数据库中插入纪录,这种主键生成机制大大限制了 JPA 提供者优化性能的可能性。在 Hibernate 中通过设置 FlushMode 为 MANUAL,可以将记录的插入延迟到长事务提交时再执行,从而减少对数据库的访问频率。实施这种系统性能提升方案的前提就是不能使用 IDENTITY 主键生成器。
SEQUENCE 主键生成器主要用在 PostgreSQL、Oracle 等自带 Sequence 对象的数据库管理系统中,它每次从数据库 Sequence 对象中取出一段数值分配给新生成的实体对象,实体对象在写入数据库之前就会分配到相应的主键。
上面的分析中,我们把现实世界中的关系数据库分成了两大类:一是支持 IDENTITY 的数据库,二是支持 SEQUENCE 的数据库。对支持 IDENTITY 的数据库来说,使用 JPA 时变得有点麻烦:出于性能考虑,它们在选用主键生成策略时应当避免使用 IDENTITY 和 AUTO,同时,他们不支持 SEQUENCE。看起来,四个主键生成器里面排除了三个,剩下唯一的选择就是 TABLE。由此可见,TABLE 主键生成机制在 JPA 中地位特殊。它是在不影响性能情况下,通用性最强的 JPA 主键生成器。
TableGenerator 有新旧之分?
JPA 的 @TableGenerator 只是通用的注解,具体的功能要由 JPA 提供者来实现。Hibernate 中实现该注解的类有两个,一是原有的 TableGenerator,类名为 org.hibernate.id.TableGenerator,这是默认的 TableGenerator。二是新 TableGenerator,指的是 org.hibernate.id.enhanced.TableGenerator。当用 Hibernate 来提供 JPA 时,需要通过配置参数指定使用何种 TableGenerator 来提供相应功能。
在 4.1 版本的 Hibernate Reference Manual 关于配置参数的章节中(网址可从参考资源中找到)可以找到如下说明:
我们建议所有使用 @GeneratedValue 的新工程都配置 hibernate.id.new_generator_mappings=true 。因为新的生成器更加高效,也更符合 JPA2 的规范。不过,要是已经使用了 table 或 sequence 生成器,新生成器与之不相兼容。
还可以再参考一下 HHH-4884 和 HHH-4690 ,里面有 Hibernate 开发人员对这些问题的看法。
综合这些资源,可以得到如下结论:
如果不配置 hibernate.id.new_generator_mappings=true,使用 Hibernate 来提供 TableGenerator 时,JPA 中 @TableGenerator 注解的 initialValue 参数是无效的。 Hibernate 开发人员原本希望用新 TableGenerator 替换掉原有的 TableGenerator,但这么做会导致已经使用旧 TableGenerator 的 Hibernate 工程在升级 Hibernate 后,新生成的主键值可能会与原有的主键冲突,导致不可预料的结果。为保持兼容,Hibernate 默认情况下使用旧 TableGenerator 机制。 没有历史负担的新 Hibernate 工程都应该使用 hibernate.id.new_generator_mappings=true 配置选项。
现在回到清单 1 所示的问题,要解决这个问题只需在 persistence.xml 文件中添加如下一行配置即可:
清单 3. 添加新的配置行
<property name="hibernate.id.new_generator_mappings" value="true"/>
使用新 TableGenerator 后就可以放心地在 JPA 中使用 initialValue 参数了,不过,这只是新 TableGenerator 的一个好处,我们接下来还可以看看新 TableGenerator 带来的更多用法。