1. 程式人生 > >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("[email protected]");
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("[email protected]");
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("[email protected]");
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("[email protected]");
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("[email protected]");
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("[email protected]");
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會出現空洞現象。但是,沒有關係,我們應該接受這種空洞現象;