4、JPA table主鍵生成策略(在JPA中table策略是首推!!!)


 

用 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 生成器,新生成器與之不相兼容。

    

  

綜合這些資源,可以得到如下結論(重要)

  1. 如果不配置 hibernate.id.new_generator_mappings=true,使用 Hibernate 來提供 TableGenerator 時,JPA 中 @TableGenerator 注解的 initialValue 參數是無效的。
  2. Hibernate 開發人員原本希望用新 TableGenerator 替換掉原有的 TableGenerator,但這么做會導致已經使用舊 TableGenerator 的 Hibernate 工程在升級 Hibernate 后,新生成的主鍵值可能會與原有的主鍵沖突,導致不可預料的結果。為保持兼容,Hibernate 默認情況下使用舊 TableGenerator 機制。
  3. 沒有歷史負擔的新 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 }
1次EntityManagerFactory,3次EntityManager。不會出現id空洞

執行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 }
3次EntityManagerFactory,3次EntityManager:會出現id空洞

刪除上次運行以后的數據表。執行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 里面有很多實用性很強的文檔

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM