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 生成器,新生成器與之不相相容。
綜合這些資源,可以得到如下結論(重要):
- 如果不配置 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("[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會出現空洞現象。但是,沒有關係,我們應該接受這種空洞現象;