根據(jù)JPA規(guī)范写妥,Entity
是滿足以下要求的Java類:
- 帶注釋
@Entity
注記 - 沒有args構(gòu)造函數(shù)啄糙。
- 不是最終的
- 具有帶注釋的ID字段(或多個字段)
@Id
如您所見扎拣,需要ID啥容。那是為什么骚亿?
我們?yōu)槭裁匆贘PA實體中使用ID呢曲秉?
JDBC和關(guān)系數(shù)據(jù)庫不需要表的主鍵或唯一鍵采蚀。在使用JDBC時,我們使用自己的語言--原生SQL查詢與數(shù)據(jù)庫進(jìn)行通信岸浑。若要獲取數(shù)據(jù)集搏存,開發(fā)人員將運行SELECT
語句,該語句返回相應(yīng)的元組矢洲。要保存或更新數(shù)據(jù)璧眠,我們需要編寫另一個INSERT
或UPDATE
聲明。在應(yīng)用程序到數(shù)據(jù)庫級的通信中读虏,應(yīng)用程序中的對象與存儲在數(shù)據(jù)庫中的記錄之間沒有直接聯(lián)系责静。通常,這種映射是作為業(yè)務(wù)邏輯的一部分手動管理的盖桥。
JPA采取了不同的方法灾螃。它引入了實體-Java對象,這些對象嚴(yán)格地與數(shù)據(jù)庫中的記錄綁定在一起揩徊。因此腰鬼,JPA規(guī)范要求開發(fā)人員定義字段或一組字段,以在實體實例和特定DB記錄之間建立一對一的關(guān)聯(lián)塑荒。這樣熄赡,開發(fā)人員就可以從數(shù)據(jù)庫中獲取JPA實體,使用它們并在以后保存它們齿税,而無需調(diào)用任何JPA實體彼硫。INSERT
或UPDATE
陳述。這是允許開發(fā)人員主要關(guān)注業(yè)務(wù)邏輯的關(guān)鍵概念之一,而大多數(shù)樣板操作是由JPA實現(xiàn)本身處理的拧篮,ID是此過程的重要部分词渤。
注: ID不必映射到定義為表主鍵的列。我們需要將ID映射到唯一標(biāo)識每一行的列串绩。但是對于本文缺虐,我們將繼續(xù)交替使用術(shù)語ID和主鍵。
ID類型:我們有什么
我們需要在實體中定義ID赏参。我們有什么選擇志笼?
首先,我們可以定義一個“簡單”或“復(fù)合”結(jié)構(gòu)的ID把篓∪依#“簡單”ID由實體中的單個字段表示,復(fù)合字段由一個單獨的類表示韧掩,該類包含一組標(biāo)識實體的字段紊浩。
通常,我們對JPA實體使用簡單的ID疗锐》凰可以自動生成簡單ID(代理ID),這是處理ID值的最常用方法滑臊。生成可以發(fā)生在數(shù)據(jù)庫端(服務(wù)器端生成)或應(yīng)用程序中(客戶端生成)口芍。這兩種方法各有優(yōu)缺點。
在本文中雇卷,我們將重點討論服務(wù)器端生成的ID鬓椭。為了簡單起見,我們將使用Hibernate ORM作為所有示例的默認(rèn)JPA實現(xiàn)关划,除非我們明確提到另一個ORM小染。
生成的ID-為什么我們要關(guān)心?
ID生成事件通常只發(fā)生一次--當(dāng)我們將新實體保存到數(shù)據(jù)庫時贮折。因此裤翩,假設(shè)我們有一個不經(jīng)常創(chuàng)建多個實體的應(yīng)用程序(經(jīng)驗法則--假設(shè)每秒不超過100個實體),并且不與其他應(yīng)用程序共享數(shù)據(jù)庫调榄。在這種情況下踊赠,理論上,我們可以使用任何ID生成策略每庆。管理國家清單的應(yīng)用程序就是一個很好的例子--我們并不經(jīng)常創(chuàng)建新的國家臼疫。但是電能計量呢?如果我們有100米扣孟,每小時發(fā)送數(shù)據(jù),我們必須每小時保存100個測量數(shù)據(jù)荣赶》锛郏基本上鸽斟,我們每36秒就可以節(jié)省一次測量±担看起來不太像富蓄。幾千米呢?數(shù)以萬計慢逾?如果我們決定每10分鐘做一次測量呢立倍?一個企業(yè)要花費多少錢來停止信息系統(tǒng)來改變ID生成策略?
在實踐中侣滩,應(yīng)用程序和業(yè)務(wù)一樣趨向于增長和變化口注,這就是為什么必須選擇適當(dāng)?shù)腎D生成策略,以避免將來發(fā)生痛苦的遷移君珠。我們將在這篇文章中提到很多性能寝志,甚至我們的應(yīng)用程序也不是新的Facebook或Twitter,它們還沒有每秒節(jié)省數(shù)百萬實體策添,我們應(yīng)該提前考慮最合適的ID生成策略材部,以避免將來出現(xiàn)問題。
默認(rèn)情況下世代是如何工作的
最簡單的方法是在JPA實體中定義生成的ID唯竹,用@Id
和@GeneratedValue
注釋乐导。我們甚至不需要為@GeneratedValue
。默認(rèn)情況下浸颓,您將得到一個正確生成的ID字段物臂。
@Table(name = "pet")
@Entity
public class Pet {
@Id
@GeneratedValue
@Column(name = "id", nullable = false)
private Long id;
}
有兩種類型的默認(rèn)值:從一開始就不應(yīng)更改的默認(rèn)值和應(yīng)該更改的默認(rèn)值。默認(rèn)值不會破壞應(yīng)用程序猾愿,但是在生成ID的情況下鹦聪,它們工作得好嗎?讓我們看看@GeneratedValue
默認(rèn)參數(shù)值:
public @interface GeneratedValue {
GenerationType strategy() default AUTO;
String generator() default "";
}
正如我們所看到的蒂秘,我們將生成策略參數(shù)設(shè)置為AUTO
泽本。這意味著JPA提供者決定如何為ID生成一個唯一的值锰悼。讓我們從我們可以使用的策略列表開始梧税。
JPA標(biāo)準(zhǔn)除了描述AUTO
:
-
IDENTITY
-特定于數(shù)據(jù)庫的內(nèi)置用途identity
用于生成ID的列類型。 -
SEQUENCE
-使用序列生成唯一的ID值也榄。 -
TABLE
-使用模擬序列的單獨表撇贺。當(dāng)應(yīng)用程序需要ID時赌莺,JPA提供程序鎖定表行,更新存儲的ID值松嘶,并將其返回給應(yīng)用程序艘狭。與前兩種策略相比,這種策略提供了最差的性能,如果可能的話巢音,應(yīng)該避免遵倦。您可以閱讀更多有關(guān)此策略的內(nèi)容。在文件中.
依開發(fā)人員手冊官撼,如果我們使用與UUID不同的ID類型(如Long梧躺、Integer等)并將策略設(shè)置為AUTO
,Hibernate將執(zhí)行以下操作(自5.0版起):
- 嘗試使用
SEQUENCE
ID生成策略 - 如果序列不受支持(即我們使用MySQL)傲绣,它將使用
TABLE
(或IDENTITY
掠哥,在Hibernate 5.0之前)生成ID的策略
為什么Hibernate試圖使用SEQUENCE
作為默認(rèn)策略?這里的關(guān)鍵指標(biāo)是性能秃诵。這個TABLE
就業(yè)績而言续搀,戰(zhàn)略是最差的。在本文中顷链,作者使用不同的策略進(jìn)行了一些測試目代。通過更改ID生成策略,他能夠?qū)?0K實體的時間從185秒減少到4.3秒嗤练。IDENTITY
到SEQUENCE
并啟用Hibernate的一些優(yōu)化榛了。所以,這兩種退步策略(IDENTITY
和TABLE
)不會破壞應(yīng)用程序煞抬,但性能不會很好霜大。
這里的問題是,即使是SEQUENCE
表現(xiàn)不佳革答;表演將接近IDENTITY
战坤。之所以發(fā)生這種情況,是因為所有實體都使用單個數(shù)據(jù)庫序列残拐,而且序列參數(shù)不允許Hibernate應(yīng)用ID池優(yōu)化途茫。我們將查看默認(rèn)的SEQUENCE
下一節(jié)將詳細(xì)介紹行為。
結(jié)語::為ID生成策略保留默認(rèn)值可能會對我們的應(yīng)用程序性能造成負(fù)面影響溪食。對于生產(chǎn)應(yīng)用程序囊卜,我們需要將默認(rèn)值更改為更合適的東西。
順序:如何正確定義错沃?
這個SEQUENCE
策略使用一個單獨的DB對象--序列--在將數(shù)據(jù)插入數(shù)據(jù)庫之前獲取和分配一個唯一的ID值栅组。這提供了批處理。INSERT
操作支持枢析,因為JPA提供程序不需要在每個提供程序之后獲取生成的ID玉掸。INSERT
與標(biāo)識列、觸發(fā)器生成的ID等類似醒叁。
缺省值:它們足夠好嗎司浪?
對象的默認(rèn)定義泊业。SEQUENCE
策略,我們需要寫下面的代碼断傲。實際上脱吱,這就是默認(rèn)情況下的結(jié)果。AUTO
如果我們的數(shù)據(jù)庫支持序列认罩,策略。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id", nullable = false)
private Long id;
為該定義生成的序列(如果我們在Hibernate設(shè)置中啟用自動數(shù)據(jù)庫創(chuàng)建)的SQL如下所示:
create sequence hibernate_sequence start 1 increment 1;
JPA提供程序?qū)⒅粚λ杏脩羰褂么藬?shù)據(jù)庫序列续捂。INSERT
語句垦垂,如果我們?yōu)樗?code>SEQUENCE我們應(yīng)用的策略。這可能會引起一些問題牙瓢。
首先劫拗,我們可能會耗盡序列。在大多數(shù)數(shù)據(jù)庫中矾克,最大序列值為2^63-1页慷,因此很難達(dá)到這一極限。但是胁附,產(chǎn)生大量新數(shù)據(jù)的應(yīng)用程序仍然有可能酒繁,例如物聯(lián)網(wǎng)系統(tǒng)或橫幅網(wǎng)絡(luò),每天產(chǎn)生數(shù)十億個事件控妻。
2^63-1是一個很大的數(shù)字州袒。如果我們每秒保存10.000個實體,我們需要大約2900萬年來耗盡這個序列弓候。這意味著郎哭,在大多數(shù)情況下,我們可能不會擔(dān)心序列結(jié)束菇存,但我們?nèi)匀恍枰庾R到序列是有限的夸研。
第二,演出會受到影響依鸥。默認(rèn)的序列增量設(shè)置為1亥至,這將禁用Hibernate對序列的ID池生成優(yōu)化。JPA提供程序?qū)拿總€INSERT
序列中的語句毕籽。例如抬闯,如果我們試圖保存兩個實體并查看Hibernate SQL日志,我們將看到如下內(nèi)容:
select nextval ('hibernate_sequence')
insert into pet (name, id) values (?, ?)
select nextval ('hibernate_sequence')
insert into pet (name, id) values (?, ?)
因此关筒,我們通過執(zhí)行兩個ID值來選擇兩個ID值溶握。SELECT
語句,將這些ID分配給實體蒸播,然后保存它們睡榆。這給了我們一個額外的開銷SELECT
每一個INSERT
萍肆。這是對應(yīng)用程序性能的負(fù)面影響。
結(jié)語::SEQUENCE
對于非數(shù)據(jù)密集型應(yīng)用程序胀屿,ID生成策略是一種很好的方法塘揣。如果我們計劃做更大的事情,為了避免性能和順序耗盡的問題宿崭,我們需要改變默認(rèn)策略的設(shè)置亲铡。
序列:我們可以改變什么?
讓我們首先指定一個實體ID生成的專用序列葡兑。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;
對于這個定義奖蔓,我們將看到執(zhí)行以下SQL:
create sequence pet_seq start 1 increment 50
Hibernate對非默認(rèn)序列使用ID生成池優(yōu)化。這樣做的目的是為一個會話分配一系列的值讹堤,并將這些值用作ID吆鹤。默認(rèn)情況下,分配的ID數(shù)等于50個洲守。
優(yōu)化的工作方式如下:
-
步驟1:Hibernate執(zhí)行一個
SELECT
從序列中獲取ID疑务。 - 步驟2如果所選值等于序列初始值,Hibernate將從序列中選擇下一個ID作為高值梗醇,將初始值設(shè)置為范圍低值知允。否則,它將進(jìn)入第4步婴削。
-
步驟3:Hibernates插入數(shù)據(jù)分配ID
low
到``high`‘范圍廊镜。 -
步驟4:一旦Hibernate需要下一個批處理,它就會從序列中選擇下一個ID值(大于初始值)唉俗。Hibernate根據(jù)
allocationSize
參數(shù)嗤朴。Low
值=ID – allocationSize
,high
=ID
。然后Hibernate進(jìn)入步驟3虫溜。
我們只多表演兩場SELECT
S用于前50個保存的實體雹姊,用于默認(rèn)設(shè)置。對于以下50個實體衡楞,我們只執(zhí)行一個額外的選擇吱雏。例如,如果啟用Hibernate SQL日志瘾境,我們可以看到如下內(nèi)容:
select nextval ('pet_seq'); //selects 1 – got initial value, need to select next value
select nextval ('pet_seq'); //selects 51 as range high value
insert into pet (name, id) values (?, ?);// id=1
insert into pet (name, id) values (?, ?);//id=2
//insert other 48 entities
select nextval ('pet_seq'); //selects 101 as range next high value, calculates 101 – 50 = 51 as the low
insert into pet (name, id) values (?, ?);//id=51
//etc.
有一個缺點:如果關(guān)閉數(shù)據(jù)庫會話(即重新啟動應(yīng)用程序或重新打開實體管理器)歧杏,將丟失未使用的ID。這樣一個短命的應(yīng)用程序的一個很好的例子可能是一個無服務(wù)器的lambda函數(shù)迷守。如果每個會話只保存一個實體犬绒,然后退出應(yīng)用程序,我們將永遠(yuǎn)失去49個ID兑凿。這種行為可能導(dǎo)致序列耗盡凯力,因此對于處理少量實例的短會話茵瘾,我們需要設(shè)置更小的ID分配大小,以避免浪費大量ID咐鹤。
要調(diào)整ID生成參數(shù)拗秘,即減少分配大小,可以使用@SequenceGenerator
注釋序列生成器允許我們使用現(xiàn)有序列或創(chuàng)建具有所需參數(shù)的新序列祈惶。例如雕旨,在下面的代碼中,我們提供完整的序列定義奸腺,并將ID分配大小指定為20。
@Id
@SequenceGenerator(name = "pet_seq",
sequenceName = "pet_sequence",
initialValue = 1, allocationSize = 20)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;
如果序列不存在血久,Hibernate將為該定義生成以下SQL:
create sequence pet_sequence start 1 increment 20
定義序列生成器時,需要記住以下內(nèi)容:如果指定現(xiàn)有序列名稱帮非,并且啟用Hibernate模式驗證氧吐,則allocationSize
參數(shù)必須與increment
參數(shù),否則應(yīng)用程序?qū)o法啟動末盔。
如果要更改Hibernate中的序列驗證行為筑舅,可以禁用模式驗證或設(shè)置參數(shù)的hibernate.id.sequence.increment_size_mismatch_strategy
價值對價值LOG
或FIX
.
為LOG
參數(shù)值時,Hibernate將忽略不匹配陨舱。這可能會導(dǎo)致PK唯一性沖突翠拣,因為ID分配范圍計算將與實際序列不匹配。increment
值游盲,我們可以得到重復(fù)的ID值误墓。例如,對于allocationSize
等于20和序列increment
是1益缎,我們會得到這樣的東西:
select nextval ('pet_seq'); // selects 1 initial value, need to select next value
select nextval ('pet_seq'); //selects 2 as range high value
insert into pet (name, id) values (?, ?);// id=1
insert into pet (name, id) values (?, ?);//id=2
//Now we’ve exceeded high value, need to select the next batch
select nextval ('pet_seq'); //selects 3 as range high value, calculates 3 – 20 = -17 as the low
insert into pet (name, id) values (?, ?);//id=-17
insert into pet (name, id) values (?, ?);//id=-16
//Restarting the application
select nextval ('pet_seq'); //selects 4 as range high value, calculates 4 – 20 = -16 as the low
insert into pet (name, id) values (?, ?);//id=-16 getting unique constraint violation
假設(shè)我們將參數(shù)設(shè)置為FIX
谜慌。在這種情況下,allocationSize
將自動調(diào)整jpa序列生成器中的參數(shù)以匹配DB序列莺奔。increment
參數(shù)欣范,例如,對于上述情況令哟,參數(shù)為1恼琼。
的另一個特性@SequenceGenerator
定義是,通過在不同的序列生成器中指定相同的“序列名稱”屏富,我們可以為不同的實體重用相同的序列晴竞。
//ID Definition for ‘Pet’ entity
@Id
@SequenceGenerator(name = "pet_seq", sequenceName = "common_sequence")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq")
@Column(name = "id", nullable = false)
private Long id;
//ID Definition for ‘Owner’ entity
@Id
@SequenceGenerator(name = "owner_seq", sequenceName = " common_sequence ")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "owner_seq")
@Column(name = "id", nullable = false)
private Long id;
結(jié)語*定義序列生成器使我們能夠:
- 使用ID獲取優(yōu)化以獲得更好的應(yīng)用程序性能。
- 根據(jù)應(yīng)用程序工作負(fù)載優(yōu)化獲取大小役听,以便在頻繁獲取ID和由于會話關(guān)閉而浪費一些ID之間保持平衡颓鲜。
- 在不同實體之間共享相同的序列表窘。
這使得SEQUENCE
ID生成幾乎是一個理想的選擇。在這個策略中有什么需要注意的地方嗎甜滨?
DB的多個客戶端:這里有什么問題嗎乐严?
即使SEQUENCE
生成策略使用數(shù)據(jù)庫中的序列,它在應(yīng)用程序代碼中分配ID值衣摩。這意味著使用相同數(shù)據(jù)庫的其他應(yīng)用程序可能不知道序列的存在昂验,因此出現(xiàn)了ID生成策略。
在數(shù)據(jù)庫中使用多個客戶端可能導(dǎo)致其他DB客戶端直接分配ID而不使用序列的情況艾扮。這些ID值可能與為應(yīng)用程序中未保存的實體保留的ID值相同既琴。當(dāng)我們的應(yīng)用程序開始保存實體時,它可能會導(dǎo)致PK唯一性沖突泡嘴,并且數(shù)據(jù)不會被存儲甫恩。
結(jié)語: SEQUENCE
如果多個客戶端更新數(shù)據(jù)庫,ID的生成策略可能無法正常工作酌予。在這種情況下磺箕,ID生成應(yīng)該由數(shù)據(jù)庫控制。IDENTITY
這里的策略效果更好抛虫。
身份:利弊
IDENTITY
是用于開發(fā)人員使用MySQL數(shù)據(jù)庫生成ID的“默認(rèn)”策略松靡。由于許多RDBMS(除了MySQL)都支持用于列定義的標(biāo)識數(shù)據(jù)類型,所以我們可以在許多應(yīng)用程序中看到這種策略建椰。有時開發(fā)人員選擇它是因為“它在我以前的項目中奏效了”雕欺,而且沒有人愿意改變這個習(xí)慣,如果它成功的話棉姐。通過指定如下代碼中的策略屠列,我們獲得了一個可靠的ID生成過程,該流程管理在一個地方--數(shù)據(jù)庫谅海。
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
為每一個INSERT
語句脸哀,數(shù)據(jù)庫將自動為相應(yīng)的@Id
場。這與SEQUENCE
策略行為扭吁,如果我們定義allocationSize
等于“1”撞蜂。對于這兩種情況,我們都需要為每個INSERT
聲明侥袜。然而蝌诡,有一個不同之處。重要的是要理解使用identity
列意味著必須在已知標(biāo)識符值之前物理插入實體行枫吧。由于數(shù)據(jù)庫生成ID的值浦旱,JPA提供程序應(yīng)該在插入數(shù)據(jù)后將其返回給應(yīng)用程序。
問題是:JPA提供程序如何在插入記錄后獲取ID九杂?如果數(shù)據(jù)庫驅(qū)動程序支持JDBC3API(大多數(shù)現(xiàn)代數(shù)據(jù)庫都支持)颁湖,則會自動完成宣蠕。JPA提供程序隱式調(diào)用Statement.getGeneratedValues()
方法,該方法返回生成的值甥捺。在遮罩下抢蚀,JPA提供程序生成如下所示的SQL語句:
insert into pet (name) values (‘Buddy’) RETURNING *
假設(shè)我們使用一個舊版本的數(shù)據(jù)庫驅(qū)動程序。在這種情況下镰禾,將執(zhí)行額外的SELECT(通常由JPA提供程序執(zhí)行皿曲,但有時我們需要手動執(zhí)行)來獲取生成的值,類似于下面的代碼吴侦。這是舊PostgreSQL版本的日志屋休,它模擬IDENTITY
使用DB序列的數(shù)據(jù)類型。對于其他RDBMS备韧,SQL將類似劫樟。
insert into pet (name) values (?)
select currval('pet_id_seq')
insert into pet (name) values (?)
select currval('pet_id_seq')
此行為不允許JPA提供程序執(zhí)行批處理插入。因為提供程序需要在每個INSERT
织堂,它將批處理操作拆分為單個操作毅哗。INSERT
運算符,并在每次執(zhí)行后獲取生成的ID值捧挺。我們不能送一批INSERT
語句并獲得一批生成的ID,因為我們無法將生成的ID可靠地關(guān)聯(lián)到JPA對象尿瞭。原因是數(shù)據(jù)庫不能保證生成的id的順序與INSERT
此外闽烙,INSERT
語句不能以與批處理相同的順序執(zhí)行。因此声搁,獲得插入記錄的ID的唯一可靠方法是拆分批處理黑竞。
結(jié)語: IDENTITY
策略簡單易用,保證了可靠的應(yīng)用獨立主鍵值的生成.
從另一方面來說疏旨,這種策略在規(guī)則中提供了次優(yōu)的性能很魂。INSERT
操作和批處理INSERT
根本不支持操作。因此檐涝,建議使用IDENTITY
對于我們保存少量新數(shù)據(jù)或幾個獨立客戶端應(yīng)用程序更改數(shù)據(jù)庫的情況遏匆。
結(jié)論:序列與序列的一致性
那么,我們應(yīng)該為我們的JPA實體選擇哪種ID生成策略呢谁榜?以下是一些建議:
-
SEQUENCE
與其他策略相比幅聘,它提供了更好的整體性能。此外窃植,我們需要考慮以下幾點: - 為每個JPA實體定義一個單獨的序列是一個很好的實踐帝蒿。避免默認(rèn)的序列生成器參數(shù)。
- 我們應(yīng)該用
@SequenceGenerator
對微調(diào)序列參數(shù)的注釋巷怜。 - 我們需要根據(jù)應(yīng)用程序工作負(fù)載模式來定義批處理大小葛超。
- 我們可能更喜歡
IDENTITY
下列案件的戰(zhàn)略: - 如果數(shù)據(jù)庫不支持序列暴氏。
- 用于不經(jīng)常創(chuàng)建和保存的實體。
- 如果我們的數(shù)據(jù)庫被其他應(yīng)用程序修改绣张。
- 避
TABLE
和AUTO
如果可能的話答渔,生成策略。他們的表現(xiàn)最差胖替。
ID列表不限于簡單的服務(wù)器生成的ID研儒。在下面的文章中,我們將討論客戶端生成的ID独令,特別是UUID端朵。此外,盡管不太流行燃箭,復(fù)合ID也有一些需要學(xué)習(xí)的東西冲呢,所以我們也將討論它們。
小伙伴們?nèi)绻X得我寫的不錯招狸,不妨幫個忙敬拓,給我點個贊唄,可以讓更多的人看到這篇文章