用 table 來生成主鍵詳解
它是在不影響性能情況下,通用性最強的 JPA 主鍵生成器。這種方法生成主鍵的策略可以適用於任何數據庫,不必擔心不同數據庫不兼容造成的問題。
initialValue不起作用?
Hibernate 從 3.2.3 之后引入了兩個新的主鍵生成器 TableGenerator 和 SequenceStyleGenerator。為了保持與舊版本的兼容,這兩個新主鍵生成器在默認情況下不會被啟用,而不啟用新 TableGenerator 的 Hibernate 在提供 JPA 的 @TableGenerator 注解時會有 Bug。
這個bug是什么呢?我們將上一節中的Customer.java的getId方法做如下下 List_1 的修改:
List_1. Id的生成策略為TABLE
@TableGenerator(name="ID_GENERATOR", table="t_id_generator", pkColumnName="PK_NAME", pkColumnValue="seed_t_customer_id", valueColumnName="PK_VALUE", allocationSize=20, initialValue=10 ) @GeneratedValue(strategy=GenerationType.TABLE, generator="ID_GENERATOR") @Id public Integer getId() { return id; }
上面的@TableGenerator配置指定了initialValue=10,指定了主鍵生成列的初始值為10,這在 @TableGenerator 的 API 文檔中寫得很清楚。現在 initialValue 值設置為 10, 那么在單元測試中用 JPA 添加新的 Customer 記錄時,新記錄的主鍵會從 11 開始。但是,實際上保存到數據庫中的主鍵值確實1 !!!
也就是說,在@TableGenerator中配置的initialValue根本不起作用!!!
這實在令人困惑。其實問題出在程序所用的 JPA 提供者(Hibernate)上面。如果改用其他 JPA 提供者,估計不會出現上面的問題(未驗證)。Hibernate 之所以會出現這種情況,並非是不尊重標准,而有它自身的原因。現在,為了把問題講清楚, 有必要先談談 JPA 主鍵生成器選型的問題,了解一下 @TableGenerator 在 JPA 中的特殊地位。
JPA 主鍵生成器選型
JPA 提供了四種主鍵生成器,參看表 1:
一般來說,支持 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.3 版本的 Hibernate Reference Manual 關於配置參數的章節中(網址可從參考資源中找到)可以找到如下說明:
我們建議所有使用 @GeneratedValue 的新工程都配置 hibernate.id.new_generator_mappings=true 。因為新的生成器更加高效,也更符合 JPA2 的規范。不過,要是已經使用了 table 或 sequence 生成器,新生成器與之不相兼容。
綜合這些資源,可以得到如下結論(重要):
- 如果不配置 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 配置選項。
提出幾個疑問
現在回到上面的問題,要解決這個問題只需在 persistence.xml 文件中添加如下一行配置即可List_2:
List_2. 配置文件persistence.xml中添加一個屬性
<!-- Setting is relevant when using @GeneratedValue. It indicates whether or not the new
IdentifierGenerator implementations are used for javax.persistence.GenerationType.AUTO,
javax.persistence.GenerationType.TABLE and javax.persistence.GenerationType.SEQUENCE. Default to false to keep backward compatibility. --> <property name="hibernate.id.new_generator_mappings" value="true"/>
Customer.java的代碼只修改了getId方法的注解:
List_3. 實體Customer的主鍵生成策略采用TABLE
1 package com.magicode.jpa.helloworld; 2 3 import java.util.Date; 4 5 import javax.persistence.Column; 6 import javax.persistence.Entity; 7 import javax.persistence.GeneratedValue; 8 import javax.persistence.GenerationType; 9 import javax.persistence.Id; 10 import javax.persistence.Table; 11 import javax.persistence.TableGenerator; 12 //import javax.persistence.TableGenerator; 13 import javax.persistence.Temporal; 14 import javax.persistence.TemporalType; 15 import javax.persistence.Transient; 16 17 /** 18 * @Entity 用於注明該類是一個實體類 19 * @Table(name="t_customer") 表明該實體類映射到數據庫的 t_customer 表 20 */ 21 @Table(name="t_customer") 22 @Entity 23 public class Customer { 24 25 private Integer id; 26 private String lastName; 27 28 private String email; 29 private int age; 30 31 private Date birthday; 32 33 private Date createdTime; 34 35 /** 36 * @TableGenerator 標簽的屬性解釋: 37 * 38 * ①、allocationSize 屬性需要賦一個整數值。表示了bucket的容量。其默認值為50。 39 * ②、table 屬性用於指定輔助表的表名。這里指定為t_id_generator數據表 40 * 41 * 其基本思想就是:從table指定的輔助表中讀取一個bucket段id號范圍內的第一個數值,記為first_id。在后面持久化過程中的id號是從first_id開始依次遞增1得到 42 * 當遞增到first_id + allocationSize 的時候,就會再一次從輔助表中讀取一個first_id開始新一輪的id生成過程。 43 * 44 * 我們知道,要從數據庫中確定一個值,則必須確定其“行”和“列”。JPA自動產生的t_id_generator只有兩列。當然,如果該表 45 * 為n個表產生id,則會在t_id_generator表中保存“n行2列”。 46 * 那么,如何從數據表t_id_generator中確定出seed_id用於為Customer實體計算id呢??JPA會依據Customer實體的 47 * @TableGenerator 屬性值來依據下面的規則的到seed_id: 48 * ③、valueColumnName 屬性指定了seed_id的列名。valueColumnName="PK_VALUE"也就是指定了 49 * seed_id位於PK_VALUE列中。同時,規定了這一列必須是數值型(int,long等)。 50 * 剩下的任務就是如何從n行中確定出是哪一行?? 51 * ④、pkColumnName="PK_NAME",pkColumnValue="seed_t_customer_id" 兩個一起來確定具體的行: 52 * 在PK_NAME列中,值為seed_t_customer_id的那一行。 53 * ⑤、由上面③和④中確定出來的“行”和“列”就可以得到一個int型的整數值。這個值就是first_id。 54 * 55 * 注意:我們的數據庫中可以沒有t_id_generator這張表,JPA會自動幫助我們完成該表的創建工作。自動創建的表只有兩列: 56 * PK_NAME(VARCHAR)和PK_VALUE(int)。同時會自動添加一條記錄(seed_t_customer_id, 51) 依據優化策略的不同,輔助表中記錄的數值有區別 57 */ 58 @TableGenerator(name="ID_GENERATOR", 59 table="t_id_generator", 60 pkColumnName="PK_NAME", 61 pkColumnValue="seed_t_customer_id", 62 valueColumnName="PK_VALUE", 63 allocationSize=20, 64 initialValue=10 65 ) 66 @GeneratedValue(strategy=GenerationType.TABLE, generator="ID_GENERATOR") 67 @Id 68 public Integer getId() { 69 return id; 70 } 71 72 /** 73 * @Column 指明lastName屬性映射到表的 LAST_NAME 列中 74 * 同時還可以指定其長度、能否為null等數據限定條件 75 */ 76 @Column(name="LAST_NAME", length=50, nullable=false) 77 public String getLastName() { 78 return lastName; 79 } 80 81 /** 82 * 利用 @Temporal 來限定birthday為DATE型 83 */ 84 @Column(name="birthday") 85 @Temporal(TemporalType.DATE) 86 public Date getBirthday() { 87 return birthday; 88 } 89 90 /* 91 * 通過 @Column 的 columnDefinition 屬性將CREATED_TIME列 92 * 映射為“DATE”類型 93 */ 94 @Column(name="CREATED_TIME", columnDefinition="DATE") 95 public Date getCreatedTime() { 96 return createdTime; 97 } 98 99 /* 100 * 通過 @Column 的 columnDefinition 屬性將email列 101 * 映射為“TEXT”類型 102 */ 103 @Column(columnDefinition="TEXT") 104 public String getEmail() { 105 return email; 106 } 107 108 /* 109 * 工具方法,不需要映射為數據表的一列 110 */ 111 @Transient 112 public String getInfo(){ 113 return "lastName: " + lastName + " email: " + email; 114 } 115 116 public int getAge() { 117 return age; 118 } 119 120 public void setId(Integer id) { 121 this.id = id; 122 } 123 124 public void setLastName(String lastName) { 125 this.lastName = lastName; 126 } 127 128 public void setEmail(String email) { 129 this.email = email; 130 } 131 132 public void setAge(int age) { 133 this.age = age; 134 } 135 136 public void setBirthday(Date birthday) { 137 this.birthday = birthday; 138 } 139 140 public void setCreatedTime(Date createdTime) { 141 this.createdTime = createdTime; 142 } 143 144 }
main方法如下,每次只需會連續保存兩條記錄。代碼如下:
List_4. 測試main方法
1 package com.magicode.jpa.helloworld; 2 3 import java.util.Date; 4 5 import javax.persistence.EntityManager; 6 import javax.persistence.EntityManagerFactory; 7 import javax.persistence.EntityTransaction; 8 import javax.persistence.Persistence; 9 10 public class Main { 11 12 public static void main(String[] args) { 13 14 /* 15 * 1、獲取EntityManagerFactory實例 16 * 利用Persistence類的靜態方法,結合persistence.xml中 17 * persistence-unit標簽的name屬性值得到 18 */ 19 EntityManagerFactory emf = 20 Persistence.createEntityManagerFactory("jpa-1"); 21 22 // 2、獲取EntityManager實例 23 EntityManager em = emf.createEntityManager(); 24 25 // 3、開啟事物 26 EntityTransaction transaction = em.getTransaction(); 27 transaction.begin(); 28 29 // 4、調用EntityManager的persist方法完成持久化過程 30 //保存第1條記錄 31 Customer customer = new Customer(); 32 customer.setAge(9); 33 customer.setEmail("Tom@163.com"); 34 customer.setLastName("Tom"); 35 customer.setBirthday(new Date()); 36 customer.setCreatedTime(new Date()); 37 em.persist(customer); 38 39 //保存第2條記錄 40 customer = new Customer(); 41 customer.setAge(10); 42 customer.setEmail("Jerry@163.com"); 43 customer.setLastName("Jerry"); 44 customer.setBirthday(new Date()); 45 customer.setCreatedTime(new Date()); 46 em.persist(customer); 47 48 // 5、提交事物 49 transaction.commit(); 50 // 6、關閉EntityManager 51 em.close(); 52 // 7、關閉EntityManagerFactory 53 emf.close(); 54 55 } 56 }
現在看執行效果,會發現一個問題。
執行第一次以后兩個數據表的狀態如下:
Figure_1. 數據表t_customer:
Figure_2. 數據表 t_id_generator:
從Figure_1我們似乎能看出某些地方和我們最初想的不一樣:@TableGenerator中指定了allocationSize=20,那么不應該是第一條記錄為11,第二條記錄為11+20=31才對嗎?現在為什么是12呢??如果說這里的12是正確的,那么allocationSize=20的作用在哪里體現呢??還有一個就是figure2中的PK_VALUE的值為什么為51,有什么講究嗎??
帶着上面的這些疑問我們在第一次運行的基礎之上將main方法運行第二次得到結果如下:
Figure_3. 數據表t_customer:
figure 4. 數據表t_id_generator:
這一次有意思了!!我們從 Figure_3 中看到第二次運行中持久化的第一條記錄的id為11+20=31,這么說來allocationSize=20的作用是在這里體現的不成?? Figure_3 難道是在告訴我們,allocationSize=20的意思是后一次EntityManagerFactory(確實是EntityManagerFactory,而不是。后面會有一個簡單的驗證過程)生命周期會在上一次生命周期的第一個id值上增加20,是這樣的嗎??還有一個問題就是,Figure_4的值是51+20=71。
上面的問題歸根到底是一個問題:@TableGenerator 注解的 allocationSize 屬性值的作用是什么??
上面講到Hibernate引入了新的TableGenerator實現類。下面先看看有哪些新的用法,然后再講解關於allocationSize 的問題:
新 TableGenerator 的更多用法
新 TableGenerator 除了實現 JPA TableGenerator 注解的全部功能外,還有其他 JPA 注解沒有包含的功能,其配置參數共有 8 項。新 TableGenerator 的 API 文檔詳細解釋了這 8 項參數的含義,但很奇怪的是,Hibernate API 文檔中給出的是 Java 常量的名字,在實際使用時還需要通過這些常量名找到對應的字符串,非常不方便。用對應字符串替換常量后,可以得到下面的配置參數表:
在描述各個參數的含義時,表中多次提到了“序列”,在這個表里的意思相當於 sequence,也相當於 segment。這里反映出術語的混亂,如果在 Hibernate 文檔中把兩個英文單詞統一起來,閱讀的時候會更加清楚。新 TableGenerator 的 8 個參數可分為兩組,前 5 個參數描述的是輔助表的結構,后 3 個參數用於配置主鍵生成算法。
先來看前 5 個參數,下圖是本文示例程序用於主鍵生成的輔助表,把圖中的元素和新 TableGenerator 前 4 個配置參數一一對應起來,它們的含義一目了然。
Figure 5. 輔助表
第 5 個參數 segment_value_length 是用來確定segment_value的長度,即序列名所能使用的最大字符數。從這 5 個參數的含義可以看出,新 TableGenerator 支持在同一個表中放下多個主鍵生成器,從而避免數據庫中為生成主鍵而創建大量的輔助表。
后面 3 個參數用於描述主鍵生成算法。第 6 個參數指定初始值。第 7 個參數 increment_size 確定了步長。最關鍵的是第 8 個參數 optimizer。optimizer 的默認值一欄寫的是“依 increment_size 的取值而定”,到底如何確定呢?
為搞清楚這個問題,需要先來了解一下 Hibernate 自帶的 Optimizer。
Hibernate 自帶的 Optimizer
Optimizer 可以翻譯成優化器,使用優化器是為了避免每次生成主鍵時都會訪問數據庫。從 Hibernate 官方文檔中找不到優化器的說明,需要查閱源碼,在org.hibernate.id.enhanced.OptimizerFactory 類中可以找到這些優化器的名字及對應的實現類,其中優化器的名字就是新 TableGenerator 中 optimizer 參數中能夠使用的值:
Hibernate 自帶了 5 種優化器,那么現在就可以加到上一節提到的問題了:默認情況下,新 TableGenerator 會選擇哪個優化器呢?
又一次,在 Hibernate 文檔中找不到答案,還是要去查閱源碼。通過分析 TableGenerator,可以看到 optimizer 的選擇策略。具體過程可用下圖來描述:
Figure 6. 選定優化器的過程
可以看出,hilo 和 legacy-hilo 兩種優化器,除非指定,一般不會在實踐中出現。接下來很重要的一步就是判斷 increment_size 的值,如果 increment_size 不做指定,使用默認的 1,那么最終選擇的優化器會是“none”。選中了“none”也就意味着沒有任何優化,每次主鍵的生成都需要訪問數據庫。這種情況下 TableGenerator 的優勢喪失殆盡,如果再用同一張表生成多個實體的主鍵,構造出來的系統在性能上會是程序員的噩夢。
在 increment_size 值大於 1 的情況下,只有 pooled 和 pooled-lo 兩種優化器可供選擇,選擇條件由布爾型參數 hibernate.id.optimizer.pooled.prefer_lo 確定,該參數默認為 false,這也意味着,大多數情況下選中的優化器會是 pooled。
我們不去討論 none 和 legacy-hilo,前者不應該使用,后者的名字看上去像是古董。剩下 hilo、pooled 和 pooled-lo 其實是同一種算法,它們的區別在於主鍵生成輔助表的數值。
Optimizer 究竟在表(輔助表)中記錄了什么?
在表 3 中提到 hilo 優化器在輔助表中的數值是 bucket 的序號。這里 bucket 可以翻譯成“桶”,也可翻譯成“塊”,其含義就是一段連續可分配的整數,如:1-10,50-100 等。桶的容量即是 increment_size 的值,假定 increment_size 的值為 50,那么桶的序號和每個桶容納的整數可參看下表:
hilo 優化器把桶的序號放在了數據庫輔助表中,pooled-lo 優化器把下一個桶的第一個整數放在數據庫輔助表中,而 pooled 優化器則把下下桶的第一個整數放在數據庫輔助表中。
從這里就可以解釋Figure 1 和 Figure 2 的現象了:Figure 1中的第一個id號是11,在實體類中設置了allocationSize=20, 而Figure 2的數據庫輔助表中記錄的數據是51。這里的51=11+20+20,也就是下下桶的第一個整數。說明采用了pooled優化器。
我們可以理解的是:在這種優化策略之下,JPA在生成id的時候每20條記錄(由allocationSize這個容量參數來決定,如:11~30,31~50...等)中僅僅需要讀取一次輔助表(只需要讀取bucket內的第一個數值,它是記錄在輔助表中的,如:11,31....等)。這樣就極大的降低了輔助表的訪問次數。
舉個例子,如果 increment_size=50, 當前某實體分到的主鍵編號為 60,可以推測出各個優化器及對應的數據庫輔助表中的值。如下表所示:
一般來說,pooled-lo 比 pooled 更符合人的習慣,沒有設置 hibernate.id.optimizer.pooled.prefer_lo 為 true 時,數據庫輔助表的值會出乎人的意料。程序員看到英文單詞“pooled”,會和連接池這樣的概念聯系在一起,這里的池不過是一堆可用於主鍵分配的整數的“池”,其含義與連接池很相似。
新 TableGenerator 實例
最后,演示一下 Hibernate 新 TableGenerator 的完整功能。新 TableGenerator 的一些功能不在 JPA 中,因此不能使用 JPA 的 @TableGenerator 注解,而是要使用Hibernate 自身的 @GenericGenerator 注解。
@GenericGenerator 注解有個 strategy 參數,用來指定主鍵生成器的名稱或類名,類名是容易找到的,不過寫起來太不方便了。生成器的名稱卻不大好找,翻遍 Hibernate 的 manual,devguide,都無法找到這些生成器的名稱,最后還得去看源碼。可以在 DefaultIdentifierGeneratorFactory 類中找到新 TableGenerator 的名稱應是“enhanced-table”。配置新 TableGenerator 的例子參看 List_5 的代碼:
List_5. 配置新 TableGenerator 的代碼
1 @Entity @Table(name="emp4") 2 public class Employee4 { 3 4 @GenericGenerator( name="id_gen", strategy="enhanced-table", 5 parameters = { 6 @Parameter( name = "table_name", value = "enhanced_gen"), 7 @Parameter( name = "value_column_name", value = "next"), 8 @Parameter( name = "segment_column_name",value = "segment_name"), 9 @Parameter( name = "segment_value", value = "emp_seq"), 10 @Parameter( name = "increment_size", value = "10"), 11 @Parameter( name = "optimizer",value = "pooled-lo") 12 }) 13 @GeneratedValue(generator="id_gen") 14 @Id 15 private long id; 16 17 private String firstName; private String lastName; 18 //...... 19 }
關於空洞
不管是 hilo、還是 pooled、或者 pooled-lo,在使用過程中不可避免地會產生空洞。比如當前主鍵編號分到第 60,接下來重啟了應用程序(就是在上面mian運行兩次的效果,第二次的第一個id是從31開始,這樣中間就有很多的id號沒有使用)或者更准確的說是在一次新的EntityManagerFactory實例中(后面有一個簡單的驗證過程),Hibernate 無法記住上一次分配的數值,於是 61-100 之間的整數可能永遠都不會用於主鍵的分配。很多人會對此不適應,覺得像是丟了什么東西,應用程序也因此不夠完美。其實,仔細去分析,這種感覺只能算是人的心理不適,對程序來說,只是需要生成唯一而不重復的數值而已,數據庫記錄之間的主鍵編號是否連續根本不影響系統的使用。ORM 程序需要適應這些空洞的存在,計算機的世界里不會因為這些空洞而不夠完美。
下面有兩個示例代碼會簡單的驗證空洞是出現在不同的EntityManagerFactory生命周期中的:
List_6. 證明空洞不會出現在不同的EntityManager生命周期中:1次EntityManagerFactory周期,3次EntityManager周期

1 package com.magicode.jpa.helloworld; 2 3 import java.util.Date; 4 5 import javax.persistence.EntityManager; 6 import javax.persistence.EntityManagerFactory; 7 import javax.persistence.EntityTransaction; 8 import javax.persistence.Persistence; 9 10 public class Main { 11 12 /** 13 * 測試: 14 * 1次EntityManagerFactory生命周期,3次EntityManager 15 * 生命周期。id分配上面不會出現空洞。 16 */ 17 public static void main(String[] args) { 18 19 /* 20 * 1、獲取EntityManagerFactory實例 21 * 利用Persistence類的靜態方法,結合persistence.xml中 22 * persistence-unit標簽的name屬性值得到 23 */ 24 EntityManagerFactory emf = 25 Persistence.createEntityManagerFactory("jpa-1"); 26 27 // 注意for的位置 28 for(int i = 0; i < 3; i++){ 29 // 2、獲取EntityManager實例 30 EntityManager em = emf.createEntityManager(); 31 32 // 3、開啟事物 33 EntityTransaction transaction = em.getTransaction(); 34 transaction.begin(); 35 36 // 4、調用EntityManager的persist方法完成持久化過程 37 //保存第1條記錄 38 Customer customer = new Customer(); 39 customer.setAge(9); 40 customer.setEmail("Tom@163.com"); 41 customer.setLastName("Tom"); 42 customer.setBirthday(new Date()); 43 customer.setCreatedTime(new Date()); 44 em.persist(customer); 45 46 //保存第2條記錄 47 customer = new Customer(); 48 customer.setAge(10); 49 customer.setEmail("Jerry@163.com"); 50 customer.setLastName("Jerry"); 51 customer.setBirthday(new Date()); 52 customer.setCreatedTime(new Date()); 53 em.persist(customer); 54 55 // 5、提交事物 56 transaction.commit(); 57 // 6、關閉EntityManager 58 em.close(); 59 } 60 61 // 7、關閉EntityManagerFactory 62 emf.close(); 63 64 } 65 }
執行List_6的示例代碼以后數據表的狀態如下:
Figure_7. 沒有id空洞出現
Figure_8. 說明只是讀(修改,讀的同時就會修改)了一次輔助表
List_7. 證明id空洞會出現在不同的EntityManagerFactory生命周期中:3次EntityManagerFactory周期,3次EntityManager周期

1 package com.magicode.jpa.helloworld; 2 3 import java.util.Date; 4 5 import javax.persistence.EntityManager; 6 import javax.persistence.EntityManagerFactory; 7 import javax.persistence.EntityTransaction; 8 import javax.persistence.Persistence; 9 10 public class Main { 11 12 /** 13 * 測試: 14 * 3次EntityManagerFactory生命周期,3次EntityManager 15 * 生命周期。id分配上面會出現空洞。 16 */ 17 public static void main(String[] args) { 18 19 // 注意for的位置 20 for(int i = 0; i < 3; i++){ 21 /* 22 * 1、獲取EntityManagerFactory實例 23 * 利用Persistence類的靜態方法,結合persistence.xml中 24 * persistence-unit標簽的name屬性值得到 25 */ 26 EntityManagerFactory emf = 27 Persistence.createEntityManagerFactory("jpa-1"); 28 29 // 2、獲取EntityManager實例 30 EntityManager em = emf.createEntityManager(); 31 32 // 3、開啟事物 33 EntityTransaction transaction = em.getTransaction(); 34 transaction.begin(); 35 36 // 4、調用EntityManager的persist方法完成持久化過程 37 //保存第1條記錄 38 Customer customer = new Customer(); 39 customer.setAge(9); 40 customer.setEmail("Tom@163.com"); 41 customer.setLastName("Tom"); 42 customer.setBirthday(new Date()); 43 customer.setCreatedTime(new Date()); 44 em.persist(customer); 45 46 //保存第2條記錄 47 customer = new Customer(); 48 customer.setAge(10); 49 customer.setEmail("Jerry@163.com"); 50 customer.setLastName("Jerry"); 51 customer.setBirthday(new Date()); 52 customer.setCreatedTime(new Date()); 53 em.persist(customer); 54 55 // 5、提交事物 56 transaction.commit(); 57 // 6、關閉EntityManager 58 em.close(); 59 60 // 7、關閉EntityManagerFactory 61 emf.close(); 62 } 63 } 64 }
刪除上次運行以后的數據表。執行List_7,得到運行后的狀態如下:
Figure_8. 出現了id空洞,id號分為三段
Figure_8. 輔助表的狀態,說明輔助表生成之后更新了兩次
結果討論如下:
①、從List_6和List_7的運行狀態可以印證id空洞是出現在不同EntityManagerFactory生命周期中的,而不是出現在EntityManager中的。也就是說,輔助表的讀取優化是在EntityManagerFactory這個層面上完成的。
②、同時也印證了上面闡述的理論,第一個id分配是從 initialValue + 1 開始的;輔助表記錄了下下一個段的first_id(依據不同的策略也可能是下一個段的first_id);
③、每一次EntityManagerFactory生命周期中,當第一次用到某個輔助表的時候,首先會檢測指定的輔助表是否存在。如果存在,則讀取first_id,同時更新輔助表的數據;如果不存在,則會創建一個輔助表,同時在輔助表中存放數值 initialValue + 1 + locationSize * 2 。
綜合上面的介紹,理一下TABLE id生成的過程(重要,上面這么多東西就為了理解這個結論):
1、JPA有4中id的生成策略。TABLE策略只是其中的一種,由於其通用性和對算法的優化,這種策略成為JPA id生成策略中的最優選擇。
2、TABLE策略的思想是這樣的:
①、TABLE將整數分成若干個段segment;
②、專門用一張數據表(稱為“輔助表”)來存放“下一個segment,或者是下下一segment”起始編號,我把它稱為first_id;
③、initialValue設置一個初始值,實際上也就是指定了第一個segment的first_id為 initialValue + 1 ;
④、allocationSize設置了segment的長度。
⑤、假如initialValue=10,allocationSize=20,那么會有兩個結論, a. 最小的一個id號就是11; b. 分段情況為 11~30,31~50,51~70 ...等;
⑥、如果當前正在使用的是11~30這個id段,那么輔助表中存放的數值會是12、31或51。
保存12的情況:在allocationSize<=1的情況下JPA的實現Hibernate不使用任何的id生成優化策略,輔助表中記錄的就是下一個要生成的id號。這樣,每次生成id都會訪問輔助表,極大的降低了效率;
保存31的情況:設置了hibernate.id.optimizer.pooled.prefer_lo為true,hibernate使用pooled-lo優化器,輔助表中存放的數值是下一個id段的起始值;
保存51的情況:沒有設置hibernate.id.optimizer.pooled.prefer_lo為true的時候,hibernate使用pooled優化器,輔助表中保存的數值是下下一個id段的起始值;
⑦、生成id號的時候,每個segment長度僅僅需要訪問一次輔助表,極大的降低了訪問輔助表的次數:每次生成id號的時候都是在first_id(如,11,31,51...)的基礎之上遞增得到的。只有當前段內的id號分配完了,才會再一次訪問輔助表得到新的first_id,開始新的一輪分配。
⑧、要想分得上述優化紅利,則必須在persistence.xml中配置<property name="hibernate.id.new_generator_mappings" value="true"/>使用新的TableGenerator類來實現@TableGenerator注解。也只有使用了該配置,initialValue屬性也才會發揮作用。
⑨、所以,allocationSize實際上是一個容量參數,是優化器的優化參數。另外,在不同的EntityManagerFactory生命周期中,持久化對象的id會出現空洞現象。但是,沒有關系,我們應該接受這種空洞現象;
注:主要參考IBM文檔庫中的一篇博文 “探索 Hibernate 新 TableGenerator 機制”
博文地址為:http://www.ibm.com/developerworks/cn/java/j-lo-tablegenerator/
另外:IBM文檔庫 http://www.ibm.com/developerworks/cn/views/java/libraryview.jsp 里面有很多實用性很強的文檔