1.主鍵生成策略
1.1 主鍵的兩種類型
- 自然主鍵:把數據表中的某一業務字段作為表的主鍵。如一張用戶表中,把用戶的用戶名作為用戶表的主鍵。這樣做的前提條件是,1.用戶的用戶名不能為空,2.用戶的用戶名不能重復,用戶的用戶名不能修改。這樣盡管也是可以的,但不能很好的滿足業務需求的改變,所以不推薦使用自然主鍵的方式。
- 代理主鍵:單獨為數據表設置一個字段作為數據表的主鍵。作為主鍵的這個字段沒有業務含義,一般直接取名為id,通常為整數類型,因為整型要比字符型節省數據庫的空間,所以一般都是使用代理主鍵的方式設置數據表的主鍵。
注意:在開發中,建議使用代理主鍵。
1.2 hibernate 中主鍵的生成策略
- assigned 自然主鍵類型
在程序中設置主鍵。如果在映射表中不設置generator
屬性,hibernate 默認使用該主鍵生成策略。但不建議使用這種方式,盡量要減少手動對主鍵的操作。 - increment 代理主鍵類型
用于整型類型,由 hibernate 自動以遞增的方式生成,每次增量為一,但只有當沒有其他進程相同一張表中插入數據時,才可以使用,不能在集群環境下使用。 - identity 代理主鍵類型
由底層數據庫設置主鍵,與 hibernate 無關。但前提是使用的數據庫要支持自動增長數據類型,如 MySQL 是支持主鍵自動生成的,但 Oracle 就不支持主鍵自動生成。如果數據庫支持主鍵自增,是可以采用該主鍵生成策略的。 - sequence 代理主鍵類型
由底層數據庫根據序列生成主鍵,與 hibernate 無關。但前提是數據庫要支持序列,Oracle 是支持的。如果數據庫支持序列,是可以采用該主鍵生成策略的。 - hilo 代理主鍵類型
hibernate 生成主鍵,hilo 是 high low (高低位方式)的縮寫,是 hibernate 常用的一種生成方式,需要一張額外的表來保存 hi(高位)的值,并手動設置 max_lo 的值,然后通過算法公式(hi * (max_lo + 1) + 0)來生成主鍵。這種生成策略可以跨數據庫,但由hilo算法生成的標志只能保證在一個數據庫是唯一的。 - natve 代理主鍵類型
根據底層數據庫,自動選擇identity、sequence、hilo 策略。但由于生成策略的控制權在 hibernate 手上,不建議采用,并且這種生成策略效率比較低。 - uuid 代理主鍵類型
由 hibernate 使用 128 為的UUID算法來生成標識符(主鍵),該算法可以在網絡環境中生成唯一字符串的標識符。長度是一個 32 位的十六進制字符串,占用控空間比較大,對應數據庫的char/varchar類型。這種生成策略與數據庫無關,所以可以跨數據庫,方便數據庫移植,效率也很高,因為不訪問數據庫就可以生成主鍵值,并且可以保證唯一性。
2.持久化類
2.1 持久化類的編寫規則
實體類經過 hibernate 操作轉換成持久化類,下面還是使用實體類說明規則。
- 實體類提供無參的構造方法。
無參的構造方法就算是不寫也可以,因為 jdk 會幫我們做,但最好加上這個無參的構造方法。 - 實體類的屬性要是私有的,并使用公開的 set 和 get 方法操作
hibernate 在底層會將查詢到的數據進行封裝,使用反射生成類的實例。 - 實體類中要有屬性作為唯一值
hibernate 要通過唯一的標識區分內存中是否有一個持久化類,在 java 中是通過地址區分是否是同一個對象的,在關系型數據庫的表中是通過主鍵區分是否有一條記錄的,在內存中,hibernate 是不允許出現兩個OID (對象唯一標識符)相同的持久化類的。 - 實體類屬性的基本類型建議使用基本數據類型的包裝類
包裝類和基本數據類型的默認值是不同的,比如 int 類型的默認值是 0,Integer 類型的默認值是 null。并且包裝類的語義描述比基本數據類型更加清晰,比如,一個學生的成績,可以是 0 分,也可以是 100 分,但如果這個學生沒有成績,用基本的數據類型就很難表示了,但包裝類就可以用 null 來表示,這樣不會產生歧義。 - 映射的實體類不要使用final關鍵字修飾
hibernate 有延遲加載機制,這個機制會產生代理對象,產生代理對象是通過字節碼的增強技術來完成的,其實就是產生了當前類的子類對象實現的,而是用 final 關鍵字修飾就無法產生子類。
2.2 持久化類的三種狀態
- 瞬時態(臨時態)(自由態)
瞬時態是對象只是 new 了出來,在內存開辟了空間,但還沒有和 session 關聯,也即是還沒有使用 session 操作內存中的對象,這時候在數據庫里面是沒有記錄的。 - 持久態
new 出來的實體化類對象經過 session 的操作,被加入到 session 的緩存中,并且與這個對象關聯的 session 也沒有關閉,這個時候就是持久態,在數據庫中存在對應的記錄,每條記錄對應唯一的持久化對象,注意持久化對象是在還未提交事務錢就已經是持久態了。 - 托管態(游離態)(離線態)
某個持久態的實例在和 session 對象關聯后,session 被關閉時,這個對象就變成了托管態,這個對象屬性值發生改變時,hibernate 就無法檢測到,因為這個實例對象已經失去了和 session 的關聯。
關于這三種狀態的理解,可以結合下面的 curd 操作和一級緩存來理解。
3.curd 操作
實體類的代碼
package cc.wenshixin.entity;
public class Notice {
private int id; // 公告序號
private String title; // 公告標題
private String content; // 公告內容
private String people; // 發布人
private String date; // 發布日期
public Notice()
{
}
public Notice(String title, String content, String people, String date) {
super();
this.title = title;
this.content = content;
this.people = people;
this.date = date;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getPeople() {
return people;
}
public void setPeople(String people) {
this.people = people;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
@Override
public String toString() {
return "Notice [id=" + id + ", title=" + title + ", content=" + content + ", people=" + people + ", date=" + date
+ "]";
}
}
hibernate 自定義的工具類,方便操作 hibernate。
package cc.wenshixin.utility;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtility {
private static Configuration cfg = null;
private static SessionFactory sessionFactory = null;
//靜態代碼塊
static
{
//加載核心配置文件
cfg = new Configuration().configure();
sessionFactory = cfg.buildSessionFactory();
}
/*提供方法返回sessionFactory*/
public static SessionFactory getSessionFactory()
{
return sessionFactory;
}
/*提供于本地線程綁定的session方法*/
public static Session getSession()
{
return sessionFactory.getCurrentSession();
}
}
下面的操作都是使用JUnit測試工具測試的代碼。
3.1 增加操作
增加操作讓持久化類從瞬時態變為持久態。
@Test
public void testSave()
{
//得到sessionFactory對象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得session對象
Session session = sessionFactory.openSession();
//開啟事務
Transaction tx = session.beginTransaction();
/*執行curd操作*/
Notice notice = new Notice("實驗室開放", "同學們課外可自由選擇實驗", "admin", "2017-10-1");
session.save(notice);
//執行事務
tx.commit();
//關閉session和sessionFactory
session.close();
sessionFactory.close();
}
3.2 查詢操作
hibernate 的刪改操作都是基于查詢操作實現的。
@Test
public void testGet()
{
//得到sessionFactory對象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session
Session session = sessionFactory.openSession();
//開啟事務
Transaction tx = session.beginTransaction();
//執行查詢操作
Notice notice = session.get(Notice.class, 2);
//這里要先重寫toString()方法
System.out.println(notice.toString());
//提交事務
tx.commit();
//關閉session和sessionFactory
session.close();
sessionFactory.close();
}
3.3 刪除操作
下面展示了兩種方式來刪除一條記錄,但建議使用第一種,先查詢后刪除的方式,應該避免第二種直接設置主鍵對應屬性值的方式。
@Test
public void testDelete()
{
//得到sessionFactory對象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session對象
Session session = sessionFactory.openSession();
//開啟事務
Transaction tx = session.beginTransaction();
//執行刪除操作
//第一種方法
//Notice notice = session.get(Notice.class, 3);
//第二種方法
//Notice notice = new Notice();
//notice.setId(2);
session.delete(notice);
//提交事務
tx.commit();
//關閉session和sessionFactory
session.close();
sessionFactory.close();
}
3.4 修改操作
先得到持久態的對象,再對這個對象進行操作。
@Test
public void testUpdate()
{
//得到sessionFactory對象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session對象
Session session = sessionFactory.openSession();
//開啟事務
Transaction tx = session.beginTransaction();
//執行更新操作
Notice notice = session.get(Notice.class, 2);
notice.setTitle("我改變了");
session.update(notice);
//執行事務
tx.commit();
//關閉session和sessionFactory
session.close();
sessionFactory.close();
}
3.5 增加或更新操作
saveOrUpdate()方法是更具持久化對象的狀態來做增加或者更新操作的,對象如果是瞬時態,那么執行事務就做增加操作,如果對象是托管態,那么執行事務就做更新操作,但此時要注意,更新操作要把持久化類的所有屬性都設置值,否則沒有設置屬性值的字段為null,下面的代碼就會產生這種情況,所以不推薦使用托管態修改數據表種的記錄。
@Test
public void testSaveOrUpdate()
{
//得到sessionFactory對象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session對象
Session session = sessionFactory.openSession();
//開啟事務
Transaction tx = session.beginTransaction();
//執行增加或更新操作
//Notice notice = new Notice("新的公告", "公告內容", "admin", "2017-10-9");
Notice notice = new Notice();
notice.setId(4);
notice.setPeople("admin");
session.saveOrUpdate(notice);
//提交事務
tx.commit();
//關閉session和sessionFactory
session.close();
sessionFactory.close();
}
3.6 持久化類狀態之間的轉化
- 瞬時態轉其他狀態
瞬時態轉持久態:執行 session 對象的 save()方法或者 saveOrUpdate()方法
瞬時態轉托管態:為瞬時態對象設置持久化標識,也即是調用 setId()方法
Notice notice = new Notice(); //瞬時態
notice。setId(2); //托管態
- 持久態轉其他狀態
持久化對象可以通過 session 對象執行 get()和 load()方法,或者 Query 查詢(后面會說到)從數據庫種獲得。
持久態轉瞬時態:執行session的delete()方法
持久態轉托管態:執行 session 的close()、clear() 或者 evict() 方法,evict()方法用于清除一級緩沖中的某一個對象,close()方法是用來關閉 session 對象,清除整個一級緩存,clear()方法用于清除一級緩存中的所有對象。 - 托管態轉氣態狀態
托管態對象是無法直接得到的,是由其他狀態對象轉化而來的,而托管態和瞬時態的區別就是 OID 有沒有值。
托管態轉持久態:執行 session 的 update()、saveOrUpdate()或者lock()方法
托管態轉瞬時態:將托管態的持久化的 OID標識設置為 null,也即是將作為主鍵的屬性值設置為 null
注意:由于持久化態對象的值改變,其實不用調用 update()方法或者 saveOrUpdate()方法,在執行完事務后就可以自動更新數據庫的(在一級緩存中會解釋自動更新),但是還是建議把方法加上,便于閱讀代碼。
4.一級緩存
4.1 什么是一級緩存
首先我們要明白什么是緩存,數據庫本身其實就是一個文件系統,并且我們知道使用流的方式操作文件效率不高,所以我們把數據放到內存里面,這樣就可以直接讀取內存里面的數據,提高讀取的效率。
hibernate 框架提供了很多的優化方式,一級緩沖就是優化方式之一。hibernate 還有二級緩存,但現在已經不適用了,使用 redis技術來代替了。
hibernate 的一級緩存就是指 session 緩存,session 緩沖就是一塊內存空間,用來存放相互管理的 java 對象,在使用 hibernate 查詢對象時,先根據對象的 OID(唯一標識符)去一級緩存中查找,如果找到就直接從一級緩存中取出使用,不用再去數據庫查詢了,這樣就提高了查詢效率,如果一級緩存中沒有,就要去數據庫中查詢,然后把查到的數據信息放到一級緩存中。hibernate 的一級緩存的作用就是減少對數據庫的訪問。
4.2 一級緩存的特點
- 1.hibernate 的一級緩存默認時打開的。
- 2.hibernate 的一級緩存使用范圍就是 session 范圍,是從 session 創建到 session 關閉。
- 3.hibernate 的一級緩存,存儲數據必須是持久化數據。
4.3 驗證一級緩存的存在
Notice notice1 = session.get(Notice.class, 1);
System.out.println(notice1);
Notice notice2 = session.get(Notice.class, 1);
System.out.println(notice2);
//比較的是對象的指向的地址是否一樣
System.out.println(notice1==notice2);
連續執行查詢操作,觀察控制臺的輸出,發現只出現了一次查詢的 sql 語句,這就說明第二次的查詢不是在數據庫中查詢得到的,而是直接從 hibernate 的一級緩存中取的,并且比較兩個對象引用的地址也是true。
4.4 解釋持久化類自動更新
在前面我們說持久化類改變屬性值后,不需使用 update()方法就可以自動更新數據庫里面的記錄,我們需要指導 hibernate 一級緩存的內部結構。在執行完查詢操作后,把查詢到的數據放到緩沖區,并且復制一份數據到快照區,直接通過 set 方法改變持久化對象的屬性值,也會改變緩沖區里面的內容,在提交事務時比較緩沖區和快照區里面的數據是否一致,如果不一致,就更新數據庫中的記錄,并更新快照區中的數據。快照區的作用就是確保一級緩存中的數據和數據庫中的數據一致。
5.事務操作
hibernate 是 jdbc 的輕量級封裝,hibernate 的事務處理就是數據庫的事務處理。
5.1 什么是事務
在數據庫操作上,一項事務是由一條或多條操作數據庫的 sql 語句組成的一個不可分割的工作單元。只有當事務中的所有操作都正常完成,整個事務才會被提交到數據庫中。如果事務中由一項操作沒有完成,則整個事務就會被回滾。事務簡單理解起來就是,一組邏輯上的操作,組成這組操作的各個單元,要么一起成功,要么一起失敗,具有統一性。
5.2 事務的四個特性詳解
事務有很嚴格的定義,需要同時滿足下面的四個特性,這四個特性通常稱之為 ACID 特性。
- 原子型(Atomic):表示將事務中所做的操作捆綁成一個不可分割的單元,即對事務所進行的數據修改等操作,要么全部執行,要么全都不執行。
- 一致性(Consistency):表示事務完成時,必須使所有的數據都保持一致狀態。
- 隔離性(Isolation):指一個事務的執行不能被其他事務干擾,即一個事務內部的操作以及使用的數據對并發的其他事務都是隔離的,并發執行的各個事務之間不能互相干擾。
- 持久性(Durability):持久性也稱永久性,指一個事務一旦被提交,它對數據庫中的數據改變就應該是永久性的。提交后其他事務對其他操作或故障不會對它有任何影響。
5.3 事務的并發問題
在實際應用中,數據庫是要被多個用戶共同訪問的,在多個事務同時使用相同的數據時,可能會發生并發的問題。
- 臟讀:一個事務讀取到了另一個事務未提交的數據。
- 不可重復度:一個事務讀到了另一個事務已經提交的 update 的數據,導致在同一個事務中的查詢結果不一致。
- 虛讀/幻讀:一個事務讀到了另一個事務已經提交的 insert 的數據,導致在同一個事務中的多次查詢結果不一致。
5.4 事務的隔離級別
為了避免上面所說的事務并發問題發生,所以在標準的 SQL 規范中,定義了四個事務隔離級別,不同的隔離級別對事務的處理是不同的。
- 讀未提交(Read Uncommitted, 1級):一個事務在執行過程中,即可以訪問其事務未提交的新插入的數據,又可以訪問未提交的修改數據。如果一個事務已經開始寫數據,而另一個事務則不允許同時進行寫操作,但允許其他事務讀此行數據,此隔離級別可防止丟失更新。
- 已提交讀(Read Commited,2級):一個事務在執行過程中,既可以訪問其他事務成功提交的新插入的數據,又可以訪問成功修改的數據。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。此隔離級別可有效防止臟讀。
- 可重復讀(Repeated Read,4級):一個事務在執行過程中,可以訪問其他事務成功提交的新插入的數據,但不可以訪問成功修改的數據。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務,此隔離級別可有效的防止不可重復讀和臟讀。
- 序列化/串行化(Serializable,8級):提供嚴格的事務隔離,它要求事務序列化執行,事務只能一個接著一個地執行,但不能并發執行。此隔離級別可有效的防止臟讀,不可重復讀和幻讀。
事務的隔離級別是由數據庫提供的,但并不是所有數據庫都支持四種隔離級別的。在使用數據庫時,隔離級別越高,安全性越高,性能越低。在實際的開發中,不會選擇最高或者最低的隔離級別,使用數據庫默認的即可。
5.5 hibernate 事務規范代碼
在 hibernate 中,可以通過代碼來操作管理事務,如通過 Transaction tx = session.beginTransaction();
開啟一個事務,持久化操作后,通過 tx.commit();
提交事務,如果事務出現異常,要通過 tx.rollback();
操作來撤銷事務(回滾事務)。
除了在代碼中對事務開啟,提交和回滾操作外,還可以在 hibernate 的配置文件中對事務進行配置。在配置文件中,可以設置事務的隔離級別。其具體的配置方法是在 hibernate.cfg.xml 文件中的 property
標簽中進行的。配置方法:<property name="hibernate.connection.isolation">4</property>
,并且我們在進行正真的事務管理時,需要考慮到事務的應用場景,事務的控制不應該放在 DAO 層,而應該放在 Service 層調用多個 DAO 實現一個業務邏輯的操作。其實最主要的是如何保證在 Service 中開啟事務時使用的 Session 對象和 DAO 中多個操作使用的是同一個 Session 對象。
下面有兩種解決辦法。
- 可以在業務層獲取到 Session,并將 Session 作為參數傳遞給 DAO。
- 可以使用 ThreadLocal 將業務層獲取的 Session 綁定到當前線程,然后在 DAO 中獲取 Session 時都從當前的線程中獲取。
第二種方式時最優的方案,而且具體的實現,hibernate 已經在內部完成了,我們只需要配置一下。hibernate5 種提供了三種管理 Session 對象的方法。
- Session 對象的生命周期與本地線程綁定
- Session 對象的生命周期與 JTA(Java Transaction API,Java事務API,是一個Java企業版的應用程序接口)事務綁定
- hibernate 委托程序管理 Session 對象的生命周期
在 hibernate 的配置文件中,hibernate.current_session_context_class 屬性用于指定 Session 管理方式,可選值有:1. tread,Session 對象的生命周期與本地線程綁定;2. jta,Session 對象的生命周期與 JTA 事務綁定;managed,hibernate 委托程序來管理 Session 對象的生命周期。在這里我們選擇 tread 值,在 hibernate.cfg.xml 中進行配置:<property name="hibernate.current_session_context_class">thread</property>
,并且在 hibernate 中提供了 getCurrentSession()方法來創建一個 Session 和本地線程 TreadLocal 綁定的方法。
/*提供于本地線程綁定的session方法*/
public static Session getSession()
{
return sessionFactory.getCurrentSession();
}
hibernate 提供的這個與本地線程綁定的 Session 可以不用關閉,當線程執行結束后,就會自動關閉了。
下面給出事務操作的規范代碼寫法。
代碼結構如下:
try {
開啟事務
提交事務
}catch() {
回滾事務
}finally {
關閉
}
@Test
public void testTx1()
{
Session session = null;
Transaction tx = null;
try{
//得到與本地線程綁定的 Session
session = HibernateUtility.getSession();
//開啟事務
tx = session.beginTransaction();
//添加操作
Notice notice = new Notice("本地線程綁定", "規范操作", "admin", "2017-10-8");
session.save(notice);
//提交事務
tx.commit();
} catch (Exception e) {
e.printStackTrace();
//回滾事務
tx.rollback();
} finally {
//不需要關閉session
}
}
//下面的代碼只是上面代碼的對比
@Test
public void testTx2()
{
SessionFactory sessionFactory = null;
Session session = null;
Transaction tx = null;
try{
sessionFactory = HibernateUtility.getSessionFactory();
//不是與本地線程綁定的 Session,類似于單例模式。
session = sessionFactory.openSession();
//開啟事務
tx = session.beginTransaction();
//添加操作
Notice notice = new Notice("本地線程綁定", "規范操作", "admin", "2017-10-8");
session.save(notice);
//提交事務
tx.commit();
} catch (Exception e) {
e.printStackTrace();
//回滾事務
tx.rollback();
} finally {
//需要關閉session
session.close();
sessionFactory.close();
}
}
6.hibernate 查詢相關API的簡單介紹
在前面,我們只進行了簡單的 curd 操作,對于查詢操作,hibernate 還有幾種不同的 API 可以選擇使用,在這里先簡單介紹一下,在后面還會詳細敘述。
6.1 Query 對象
使用 query 對象,不需要寫 sql 語句,但要寫簡單的 hql(hibernate query language,hibernate 的查詢語言) 語句。
hql 和 sql 語句的區別:
- hql 語句是直接使用實體類和屬性來做查詢
- sql 語句是要操作數據表和字段
hql語句的寫法:from 實體類的名稱
。
Query 對象的使用:
- 創建 query 對象
- 調用 query 對象里面的方法得到結果
示例代碼如下:
@Test
//查詢表中所有數據
public void testQuery1()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Query<Notice> query = session.createQuery("from Notice");
List<Notice> list = query.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
@Test
//有條件的查詢
public void testQuery2()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Query<Notice> query = session.createQuery("from Notice where title=?");
query.setString(0, "實驗室開放");
List<Notice> list = query.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
6.2 Criteria 對象
使用 criteria 對象,不需要寫語句,直接調用方法來實現。
criteria 對象的使用:
- 創建 criteria 對象
- 調用對象里面的方法得到結果
示例代碼如下:
@Test
//查詢表中所有數據
public void testCriteria1()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Criteria criteria = session.createCriteria(Notice.class);
List<Notice> list = criteria.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
@Test
//有條件的查詢
public void testCriterial2()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Criteria criteria = session.createCriteria(Notice.class);
criteria.add(Restrictions.eq("title", "實驗室開放"));
List<Notice> list = criteria.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
6.3 SQLQuery 對象
從名字就可以看出是和 sql 有關的,直接寫 sql 語句,底層 hibernate 調用的是 sql 語句實現的。
SQLQuery 對象
- 創建 SQLQuery 對象
- 調用對象的方法得到結果
示例代碼如下:
@Test
public void testSQLQuery()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
SQLQuery sqlQuery = session.createSQLQuery("SELECT * FROM notice_content");
List<Object[]> list = sqlQuery.list();
for(Object[] objects : list)
{
System.out.println(Arrays.toString(objects));
}
}