Hibernate 中主鍵映射的助攻
1. 前言
本節(jié)課和大家一起聊聊 Hibernate 中的主鍵策略。通過(guò)本節(jié)課程,你將了解到:
- 什么是主鍵策略及主鍵生成器的種類;
- 如何映射復(fù)合主鍵。
2. 主鍵策略
Hibernate 進(jìn)行數(shù)據(jù)庫(kù)操作時(shí),可依靠主鍵生成器組件更快速、準(zhǔn)確地進(jìn)行一系列操作。這便是主鍵策略。
2.1 主鍵生成器
主鍵是關(guān)系數(shù)據(jù)庫(kù)中的概念,目的是唯一標(biāo)識(shí)表中記錄,保證實(shí)體數(shù)據(jù)的完整性。
- 關(guān)系數(shù)據(jù)庫(kù)中表與表中數(shù)據(jù)的關(guān)系描述需依賴主鍵實(shí)現(xiàn) ;
- 另有外鍵概念,所謂外鍵是在另一張表中對(duì)引用表的主鍵值的引用稱呼。
主外鍵關(guān)系指在不同的表中通過(guò)共同的字段信息建立起表中數(shù)據(jù)依賴(引用)關(guān)系。
回到 Hibernate 的世界!先展示一段代碼:
Student student = new Student(2, "Configuration老二", "男");
session.save(student);
上面的代碼功能:把應(yīng)用程序中的數(shù)據(jù)寫入到數(shù)據(jù)庫(kù)中,沒(méi)毛病呀!
來(lái)!沒(méi)毛病找點(diǎn)毛病出來(lái):
實(shí)際操作時(shí),要求 Hibernate 把程序中 stuId 屬性的值插入到表中同名的 stuId 主鍵字段中。
主鍵有什么特點(diǎn)?
唯一性!回答得對(duì)。
請(qǐng)問(wèn)在應(yīng)用程序中構(gòu)建數(shù)據(jù)時(shí),如何確保賦值給 stuId 的值在表中不存在!這就是問(wèn)題所在。
如何解決?
使用 Hibernate 主鍵生成器。
所謂主鍵生成器其作用就是在 Hibernate 向表中插入數(shù)據(jù)時(shí),負(fù)責(zé)生成表中數(shù)據(jù)記錄的主鍵。
Hibernate 主鍵生成器 API 介紹:
- Hibernate 的主鍵生成器(generator)都實(shí)現(xiàn)了 org.hibernate.id.IdentityGenerator 接口;
public class IdentityGenerator extends AbstractPostInsertGenerator { …… }
- 開(kāi)發(fā)者可以遵循這個(gè)接口規(guī)范提供自己的主鍵生成方案;
- Hibernate 內(nèi)置有較多主鍵生成器,主鍵生成器都有自己的實(shí)現(xiàn)類,并提供有快捷名稱方便在注解或 XML 中引用。
常用主鍵生成器一覽:
-
org.hibernate.id.IncrementGenerator(increment):對(duì) long、short 或 int 的數(shù)據(jù)列生成自動(dòng)增長(zhǎng)主鍵;
-
org.hibernate.id.IdentityGenerator(identity): 適用于 SQL server,MySql 等支持自動(dòng)增長(zhǎng)列的數(shù)據(jù)庫(kù),適合 long、short 或 int 數(shù)據(jù)列類型;
-
org.hibernate.id.SequenceGenerator(sequecne):適用 oracle,DB2 等支持 Sequence 的數(shù)據(jù)庫(kù),適合 long、short 或 int 數(shù)據(jù)列類型;
-
org.hibernate.id.UUIDGenerator(uuid):對(duì)字符串列的數(shù)據(jù)采用 128 - 位 uuid 算法生成唯一的字符串主鍵;
-
org.hibernate.id.Assigned(assigned):由應(yīng)用程序指定,也是默認(rèn)生成策略。
默認(rèn)使用 assigned 生成器。這種方案要求開(kāi)發(fā)者在應(yīng)用程序中提供自己的主鍵生成算法:
- 調(diào)用保存方法之前,先帶著指定的值往數(shù)據(jù)庫(kù)中跑一趟,檢索是否存在重復(fù),如果有,再試其它值;
- 調(diào)用保存方法之前,先檢索到表中 stuId 字段值的最大值,返回應(yīng)用程序后遞增 1,用于 stuId 新值。如果多個(gè)用戶同時(shí)向數(shù)據(jù)中插入數(shù)據(jù),這種方案會(huì)出問(wèn)題,不適合并發(fā)操作環(huán)境。
使用 assigned 生成器除非有一個(gè)很完美的解決方案,否則建議只用于學(xué)習(xí)或測(cè)試環(huán)境。
本課程使用的是 Mysql 數(shù)據(jù)庫(kù),最佳選擇 identity 生成器,主鍵值交給數(shù)據(jù)庫(kù)的自動(dòng)增長(zhǎng)列自動(dòng)生成。
2.2 使用主鍵生成器重構(gòu)代碼
- 在 Student 類的標(biāo)識(shí)屬性(stuId)上標(biāo)注如下注解;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public Integer getStuId() {
return stuId;
}
簡(jiǎn)單得難以置信!空靈而干凈??!
使用 @GeneratedValue 注解確定主鍵生成器類型。GenerationType 是一個(gè)枚舉類型,有如下幾個(gè)選擇:
- AUTO:Hibernate 區(qū)分?jǐn)?shù)據(jù)庫(kù)系統(tǒng),自動(dòng)選擇最佳策略;
- IDENTITY: 適合具有自動(dòng)增長(zhǎng)類型的數(shù)據(jù)庫(kù),如 MySql……
- SEQUENCE: 適合如 Oracle 類型數(shù)據(jù)庫(kù);
- TABLE: 使用 Hibernate 提供的 TableGenerator 生成器,不常用。
- 為了更好觀察生成的新數(shù)據(jù),重建數(shù)據(jù)庫(kù)中的表。主配置文件中修改或添加如下配置信息;
<property name="hbm2ddl.auto">create</property>
- 執(zhí)行插入數(shù)據(jù)實(shí)例;
// 打開(kāi)事務(wù)
try{
transaction = session.beginTransaction();
// 添加一條學(xué)生信息,此處沒(méi)有指定學(xué)生編號(hào)
Student student = new Student("Hibernate 01", "男");
session.save(student);
transaction.commit();
} catch(Exception e) {
transaction.rollback();
} finally {
session.close();
}
-
進(jìn)入 Mysql 系統(tǒng)查看,表結(jié)構(gòu)中 stuId 自動(dòng)設(shè)為主鍵,且為自動(dòng)遞增;
-
查看表中數(shù)據(jù),主鍵值自動(dòng)生成;
-
試著多加幾條數(shù)據(jù),別忘記修改如下配置信息。
<property name="hbm2ddl.auto">update</property>
大功告成??!
2.3 主鍵生成器
使用注解 @GeneratedValue 指定生成器類型后,Hibernate 一般情況下會(huì)自動(dòng)創(chuàng)建對(duì)應(yīng)的生成器對(duì)象,如前面指定類型為 IDENTITY,則創(chuàng)建生成 org.hibernate.id.IdentityGenerator 對(duì)象。
如果需要個(gè)性化定制生成器對(duì)象,則需要顯示指定生成器對(duì)象,如為 Oracle 數(shù)據(jù)庫(kù)指定主鍵生成器時(shí),則配置可如下:
XML 映射方式:
<id name="stuId" type="Integer" column="stuId">
<generator class="sequence">
<param name="sequence">mySeq</param>
</generator>
</id>
注解映射方式:
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE,generator="mySeqIdGen")
@SequenceGenerator(name="mySeqIdGen",sequenceName="mySeq")
public Integer getStuId() {
return stuId;
}
@SequenceGenerator 注解顯示指明使用 org.hibernate.id.SequenceGenerator 生成器對(duì)象,并指定使用數(shù)據(jù)庫(kù)中的命名為 mySeq 的序列化器。
其它主鍵生成器的使用本文不再?gòu)?fù)述,拋磚引玉,學(xué)習(xí)者可自行深入!
3. 復(fù)合主鍵
3.1 什么是復(fù)合主鍵
關(guān)系數(shù)據(jù)庫(kù)中,主鍵可指定一個(gè)字段實(shí)現(xiàn),也可指定多個(gè)字段實(shí)現(xiàn),這樣的主鍵叫復(fù)合主鍵。
從數(shù)據(jù)庫(kù)表設(shè)計(jì)原則分析,盡可能少用復(fù)合主鍵,但并不排除需要使用的場(chǎng)景。
對(duì)使用 Hibernate 的開(kāi)發(fā)者而言,將面對(duì)一個(gè)新問(wèn)題:在應(yīng)用程序中,如何映射表中的復(fù)合主鍵?
應(yīng)用程序中,復(fù)合主鍵映射方案有三:
- 將嵌入類注解為 @Embeddable,并將實(shí)體類的屬性注解為 @Id;
- 將實(shí)體類的屬性注解為 @EmbeddedId;
- 將實(shí)體類注解為 @IdClass,并將該實(shí)體類所有屬于主鍵的屬性都注解為 @Id
先分清楚兩個(gè)概念:
- 實(shí)體類:使用 @entity 注解的類;
- 嵌入類:使用 @Embeddable 注解的類;
3.2 復(fù)合主鍵映射方案一
實(shí)施流程
-
假設(shè)學(xué)生表中使用了 stuId,stuName 兩字段構(gòu)成復(fù)合主鍵;
-
應(yīng)用程序中構(gòu)建兩個(gè)類;
嵌入類:
@Embeddable
public class StudentId {
private Integer stuId;
private String stuName;
public StudentId() {
super();
}
public StudentId(Integer stuId, String stuName) {
super();
this.stuId = stuId;
this.stuName = stuName;
}
//……省略get、set方法
嵌入類說(shuō)明:
-
標(biāo)注有 @Embeddable;
-
類中包括 stuId、stuName 兩個(gè)屬性與表中的復(fù)合字段相呼應(yīng);
-
必須實(shí)現(xiàn) Serializable?。?!后續(xù)章節(jié)會(huì)聊到為什么。
實(shí)體類:
@Entity
public class Student_ {
private StudentId studentId;
private String stuSex;
public Student_() {
super();
}
public Student_(StudentId studentId, String stuSex) {
super();
this.studentId = studentId;
this.stuSex = stuSex;
}
@Id
public StudentId getStudentId() {
return studentId;
}
//……省略其它set、get方法
實(shí)體類說(shuō)明:
實(shí)體類使用 @Entity 注解;
關(guān)鍵代碼分析:
關(guān)鍵點(diǎn)一: 內(nèi)部添加引用嵌入類屬性。
private StudentId studentId;
關(guān)鍵點(diǎn)二: studentId 屬性上需要添加 @Id 注解。
@Id
public StudentId getStudentId() {
return studentId;
}
- 重新創(chuàng)建數(shù)據(jù)庫(kù)中的學(xué)生表:
<property name="hbm2ddl.auto">create</property>
-
運(yùn)行測(cè)試實(shí)例。
Tips: 對(duì)于復(fù)合主鍵,需要在代碼級(jí)別指定值。
// 打開(kāi)事務(wù)
transaction = session.beginTransaction();
// 添加一條學(xué)生信息
Student_ student = new Student_();
// 復(fù)合主鍵信息
StudentId studentId=new StudentId(1, "Hibernate是老大");
student.setStudentId(studentId);
student.setStuSex("男");
session.save(student);
transaction.commit();
- 查看 MySql,會(huì)發(fā)現(xiàn)新表 student_ 中指定復(fù)合主鍵,且數(shù)據(jù)添加成功。
如上所述,嵌入類就是復(fù)合主鍵映射類!
3.3 復(fù)合主鍵映射方案二
與第一方案相比,保留 @Entity 注解的實(shí)體類。
第一方案中的嵌入類上不再使用 @Embedded 注解,嵌入類降維成普通類。
實(shí)體類中不再使用 @Id 注解,而是使用 @EmbeddedId,此注解語(yǔ)義明確:一注解承擔(dān)兩注解任務(wù)。
可理解 @EmbeddedId 注解是 @Embedded 和 @Id 兩個(gè)注解的綜合體。
@EmbeddedId
public StudentId getStudentId() {
return studentId;
}
和第一方案一樣進(jìn)行代碼測(cè)試,結(jié)果沒(méi)什么不一樣。
3.4 復(fù)合主鍵映射方案三
方案三與前兩個(gè)方案區(qū)別:
- 沒(méi)有嵌入類概念,前面的嵌入類降維成一個(gè)普通類,不加任何注解描述;
此類的作用僅僅在邏輯上把兩個(gè)標(biāo)識(shí)屬性歸為一組!
public class StudentId implements Serializable{
private Integer stuId;
private String stuName;
public StudentId() {
super();
}
public StudentId(Integer stuId, String stuName) {
super();
this.stuId = stuId;
this.stuName = stuName;
}
//……省略set、get方法
- 實(shí)體類中使用 @IdClass 指定內(nèi)部有標(biāo)識(shí)屬性的類,另在實(shí)體類中也重復(fù)出現(xiàn)標(biāo)識(shí)屬性且上面使用 @Id 注解。
@Entity
//指明實(shí)體類中標(biāo)注有 @Id 的屬性為同一類型
@IdClass(StudentId.class)
public class Student_ {
private Integer stuId;
private String stuName;
private String stuSex;
public Student_() {
super();
}
public Student_(Integer stuId, String stuName, String stuSex) {
super();
this.stuId = stuId;
this.stuName = stuName;
this.stuSex = stuSex;
}
@Id
public Integer getStuId() {
return stuId;
}
@Id
public String getStuName() {
return stuName;
}
//……省略set、get方法
}
測(cè)試代碼,結(jié)果和前面 2 個(gè)方案一樣。
3.5 方案比較
通過(guò)代碼的編寫過(guò)程,3 種方案優(yōu)劣比較明顯:
- 第一種方案和第二種方案本質(zhì)上沒(méi)有太多區(qū)別,只是一個(gè)使用 @Id 和 @Embeddable 兩個(gè)注解;一個(gè)是使用 @EmbeddedId 注解行使兩個(gè)注解的功能;
- 顯然,第二種方案稍優(yōu)于第一方案,至少可少使用一個(gè)注解;
- 第三種方案代碼有重復(fù)之處,與 OOP 中的重用原則相違背,請(qǐng)慎用。
4. 小結(jié)
本節(jié)課,聊到了主鍵生成器,通過(guò)主鍵生成器這個(gè)助攻手,能有效地保持主鍵的唯一性,從而保證數(shù)據(jù)的完整性。
另聊了復(fù)合主鍵,復(fù)合主鍵映射備選方案雖多,但你可只記你心中最鐘情的那個(gè)。