查看: 2755|回复: 6

探索 Hibernate 新 TableGenerator 机制

[复制链接]
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
跳转到指定楼层
1#
发表于 2013-10-16 20:07 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

从 initialValue 说起
问题的发现源自对 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 中的特殊地位。
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
2#
 楼主| 发表于 2013-10-16 20:07 | 只看该作者
JPA 主键生成器选型
JPA 提供了四种主键生成器,参看表 1:
表 1. JPA 的四种主键生成器
生成器名称        描述
AUTO         由 JPA 提供者根据数据库自行决定生成算法。
IDENTITY         由数据库的自增列提供主键值。
SEQUENCE         由数据库 Sequence 对象提供主键值。
TABLE         由 JPA 提供者通过创建数据库表来记录生成的主键值。

SequenceStyleGenerator
Hibernate 的 SequenceStyleGenerator 允许在不支持 Sequence 对象的数据库中模拟使用 SEQUENCE 主键生成器,这种模拟的 SEQUENCE 主键生成器本质上其实还是 TABLE 生成器。默认情况下不启用该生成器。具体配置与新 TableGenerator 相似。参考资源中有相关资料。
一般来说,支持 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 主键生成器。

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
3#
 楼主| 发表于 2013-10-16 20:07 | 只看该作者
TableGenerator 有新旧之分?
TableGenerator 注解和 TableGenerator 类
这里反复提到的 TableGenerator 有两种,一是 JPA 中的注解,另一个是 Hibernate 中的实现类,阅读时需要注意区别。TableGenerator 注解是 JPA 规范中的注解,用于确定 TABLE 主键生成器的各个参数。Hibernate 中的两个 TableGenerator 类实现了 TABLE 主键生成器的功能,它们是类,不是注解。
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 带来的更多用法。

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
4#
 楼主| 发表于 2013-10-16 20:07 | 只看该作者
新 TableGenerator 的更多用法
[size=1.166em]新 TableGenerator 除了实现 JPA TableGenerator 注解的全部功能外,还有其他 JPA 注解没有包含的功能,其配置参数共有 8 项。新 TableGenerator 的 API 文档详细解释了这 8 项参数的含义,但很奇怪的是,Hibernate API 文档中给出的是 Java 常量的名字,在实际使用时还需要通过这些常量名找到对应的字符串,非常不方便。用对应字符串替换常量后,可以得到下面的配置参数表:
表 2. 新 TableGenerator 配置参数表
序号
参数名
默认值
含义
1
table_name
hibernate_sequence
辅助表的表名
2
value_column_name
next_val
存放序列值的列名
3
segment_column_name
sequence_name
存放序列名的列名
4
segment_value
default
序列名
5
segment_value_length
255
序列名所在列数据类型长度
6
initial_value
1
指定序列的初始值
7
increment_size
1
指定序列默认的递增量
8
optimizer
依 increment_size 的取值而定
指定序列的优化器

[size=1.166em]在描述各个参数的含义时,表中多次提到了“序列”,在这个表里的意思相当于 sequence,也相当于 segment。这里反映出术语的混乱,如果在 Hibernate 文档中把两个英文单词统一起来,阅读的时候会更加清楚。新 TableGenerator 的 8 个参数可分为两组,前 5 个参数描述的是辅助表的结构,后 3 个参数用于配置主键生成算法。
[size=1.166em]先来看前 5 个参数,下图是本文示例程序用于主键生成的辅助表,把图中的元素和新 TableGenerator 前 4 个配置参数一一对应起来,它们的含义一目了然。
图 1. 辅助表
[size=1.166em]第 5 个参数 segment_value_length 是用来确定序列名称所在列的长度,即序列名所能使用的最大字符数。从这 5 个参数的含义可以看出,新 TableGenerator 支持在同一个表中放下多个主键生成器,从而避免数据库中为生成主键而创建大量的辅助表。
[size=1.166em]后面 3 个参数用于描述主键生成算法。第 6 个参数指定初始值。第 7 个参数 increment_size 确定了步长。最关键的是第 8 个参数 optimizer。optimizer 的默认值一栏写的是“依 increment_size 的取值而定”,到底如何确定呢?
[size=1.166em]为搞清楚这个问题,需要先来了解一下 Hibernate 自带的 Optimizer。


使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
5#
 楼主| 发表于 2013-10-16 20:08 | 只看该作者
Hibernate 自带的 Optimizer
[size=1.166em]Optimizer 可以翻译成优化器,使用优化器是为了避免每次生成主键时都会访问数据库。从 Hibernate 官方文档中找不到优化器的说明,需要查阅源码,在 org.hibernate.id.enhanced.OptimizerFactory 类中可以找到这些优化器的名字及对应的实现类,其中优化器的名字就是新 TableGenerator 中 optimizer 参数中能够使用的值:
表 3. Optimizer 名字及实现类
Optimizer 名字
类名
特点
none
NoopOptimizer.class
没有任何优化,每次主键生成都需要访问数据库。
hilo
HiLoOptimizer.class
hilo 算法实现的主键生成机制,数据库中的值是 bucket 的序号。
legacy-hilo
LegacyHiLoAlgorithmOptimizer.class
旧 hilo 算法。
pooled
PooledOptimizer.class
也使用 hilo 算法,不同之处在于 bucket 内部数值保存在数据库中。
pooled-lo
PooledLoOptimizer.class
算法与 pooled 完全相同,但保存在数据库中的值不同于 pooled。

[size=1.166em]Hibernate 自带了 5 种优化器,那么现在就可以加到上一节提到的问题了:默认情况下,新 TableGenerator 会选择哪个优化器呢?
[size=1.166em]又一次,在 Hibernate 文档中找不到答案,还是要去查阅源码。通过分析 TableGenerator,可以看到 optimizer 的选择策略。具体过程可用下图来描述:
图 2. 选定优化器的过程[size=0.8em]关于 allocationSize
[size=1.166em]JPA 的 @TableGenerator 注解有一个参数 allocationSize,如果用 Hibernate 来提供 JPA,并且开启 new_generator_mapping 参数,那么 allocationSize 的值就会是这里的 increment_size。经常可以在网络上看到把 allocationSize 设置成 1 的例子,这种行为无异于应用程序的自残。这种情况下还不如使用 AUTO。值得庆幸的是 allocationSize 的默认值为 50。

[size=1.166em]可以看出,hilo 和 legacy-hilo 两种优化器,除非指定,一般不会在实践中出现。接下来很重要的一步就是判断 increment_size 的值,如果 increment_size 不做指定,使用默认的 1,那么最终选择的优化器会是“none”。选中了“none”也就意味着没有任何优化,每次主键的生成都需要访问数据库。这种情况下 TableGenerator 的优势丧失殆尽,如果再用同一张表生成多个实体的主键,构造出来的系统在性能上会是程序员的噩梦。
[size=1.166em]在 increment_size 值大于 1 的情况下,只有 pooled 和 pooled-lo 两种优化器可供选择,选择条件由布尔型参数 hibernate.id.optimizer.pooled.prefer_lo 确定,该参数默认为 false,这也意味着,大多数情况下选中的优化器会是 pooled。
[size=1.166em]我们不去讨论 none 和 legacy-hilo,前者不应该使用,后者的名字看上去像是古董。剩下 hilo、pooled 和 pooled-lo 其实是同一种算法,它们的区别在于主键生成辅助表的数值。


使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
6#
 楼主| 发表于 2013-10-16 20:08 | 只看该作者
Optimizer 究竟在表中记录了什么?
在表 3 中提到 hilo 优化器在辅助表中的数值是 bucket 的序号。这里 bucket 可以翻译成“桶”,也可翻译成“块”,其含义就是一段连续可分配的整数,如:1-10,50-100 等。桶的容量即是 increment_size 的值,假定 increment_size 的值为 50,那么桶的序号和每个桶容纳的整数可参看下表:
表 4. 桶的序号和容纳的整数
序号         1         2         3         4         5         6         ...
容纳的整数         1-50         51-100         101-150         151-200         201-250         251-300         ...

hilo 优化器把桶的序号放在了数据库辅助表中,pooled-lo 优化器把下一个桶的第一个整数放在数据库辅助表中,而 pooled 优化器则把下下桶的第一个整数放在数据库辅助表中。举个例子,如果 increment_size=50, 当前某实体分到的主键编号为 60,可以推测出各个优化器及对应的数据库辅助表中的值。如下表所示:
表 5. 优化器与辅助表的值
优化器名称         当前主键 60, 对应辅助表中的值
hilo         2
pooled-lo         101
pooled         151

一般来说,pooled-lo 比 pooled 更符合人的习惯,没有设置 hibernate.id.optimizer.pooled.prefer_lo 为 true 时,数据库辅助表的值会出乎人的意料。
程序员看到英文单词“pooled”,会和连接池这样的概念联系在一起,这里的池不过是一堆可用于主键分配的整数的“池”,其含义与连接池很相似。

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
7#
 楼主| 发表于 2013-10-16 20:08 | 只看该作者
新 TableGenerator 实例
关于空洞
不管是 hilo、还是 pooled、或者 pooled-lo,在使用过程中不可避免地会产生空洞。比如当前主键编号分到第 60,接下来重启了应用程序,Hibernate 无法记住上一次分配的数值,于是 61-100 之间的整数可能永远都不会用于主键的分配。很多人会对此不适应,觉得像是丢了什么东西,应用程序也因此不够完美。其实,仔细去分析,这种感觉只能算是人的心理不适,对程序来说,只是需要生成唯一而不重复的数值而已,数据库记录之间的主键编号是否连续根本不影响系统的使用。ORM 程序需要适应这些空洞的存在,计算机的世界里不会因为这些空洞而不够完美。
最后,演示一下 Hibernate 新 TableGenerator 的完整功能。新 TableGenerator 的一些功能不在 JPA 中,因此不能使用 JPA 的 @TableGenerator 注解,而是要使用 Hibernate 自身的 @GenericGenerator 注解。
@GenericGenerator 注解有个 strategy 参数,用来指定主键生成器的名称或类名,类名是容易找到的,不过写起来太不方便了。生成器的名称却不大好找,翻遍 Hibernate 的 manual,devguide,都无法找到这些生成器的名称,最后还得去看源码。可以在 DefaultIdentifierGeneratorFactory 类中找到新 TableGenerator 的名称应该是“enhanced-table”。配置新 TableGenerator 的例子参看清单 4 的代码:
清单 4. 配置新 TableGenerator 的代码
@Entity @Table(name="emp4")
public class Employee4 {
   @GenericGenerator( name="id_gen", strategy="enhanced-table",
   parameters = {
           @Parameter( name = "table_name", value = "enhanced_gen"),
           @Parameter( name ="value_column_name", value = "next"),
           @Parameter( name = "segment_column_name",value = "segment_name"),
           @Parameter( name = "segment_value", value = "emp_seq"),
           @Parameter( name = "increment_size", value = "10"),
           @Parameter( name = "optimizer",value = "pooled-lo")
   })
@Id @GeneratedValue(generator="id_gen")
private long id;
private String firstName; private String lastName;
......
}

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表