1. 程式人生 > >Hibernate學習筆記(二)—— 實體規則&物件的狀態&一級快取

Hibernate學習筆記(二)—— 實體規則&物件的狀態&一級快取

一、持久化類

1.1 什麼是持久化類?

  Hibernate是持久層的ORM對映框架,專注於資料的持久化工作。所謂的持久化,就是將記憶體中的資料永久儲存到關係型資料庫中。那麼知道了什麼是持久化,什麼又是持久化類呢?其實所謂的持久化類知道是一個Java類與資料庫表建立了對映關係,那麼這個類稱為是持久化類。其實你可以簡單的理解為持久化類就是一個Java類有了一個對映檔案與資料庫的表建立了關係。那麼我們編寫持久化類的時候有哪些要求呢?

1.2 持久化類的編寫規則

  我們在編寫持久化類的時候需要有一下幾點需要注意:

  • 持久化類需要提供無引數的構造方法。因為在Hibernate的底層需要使用反射生成類的例項。
  • 持久化類的屬性需要私有,對私有的屬性提供公有的get和set方法。因為在Hibernate底層會將查詢到的資料進行封裝。
  • 持久化類的屬性要儘量使用包裝的型別。因為包裝類和基本資料型別的預設值不同,包裝類的型別語義描述更清晰而基本資料型別不容易描述。舉個例子:
        假設表中有一列員工工資,如果使用double型別,如果這個員工工資忘記錄入到系統中,系統會將預設值0存入到資料庫,如果這個員工的工資被扣完了,也會向系統中存入0.那麼這個0就有了多重含義,而如果使用包裝類型別就會避免以上情況。如果使用Double型別,忘記錄入的工資就會存入null,而如果這個員工的工資被扣完了,就會存入0,不會產生歧義。
  • 持久化類要有一個唯一標識OID與表的主鍵對應。因為Hibernate中需要通過這個唯一標識OID區分在記憶體中是否是同一個持久化類。在Java中通過地址區分是否是同一個物件,在關係型資料庫的表中是通過主鍵區分是否是同一條記錄。那麼Hibernate就是通過這個OID來進行區分的。Hibernate是不允許在記憶體中出現兩個OID相同的持久化物件的。
  • 持久化類儘量不要使用final進行修飾。因為Hibernate中有延遲載入的機制,這個機制中會產生代理物件,Hibernate產生代理物件使用的是位元組碼的增強技術完成的,其實就是產生了當前類的一個子類物件實現的。如果使用了final修飾持久化類。那麼就不能產生子類,從而就不會產生代理物件,那麼Hibernate的延遲載入策略(是一種優化手段)就會失效。

  持久化類我們已經可以正常編寫了,但是在持久化類中需要有一個唯一標識OID與表的主鍵去建立對映關係。而且主鍵一般我們是不會讓客戶手動錄入的,一般我們是由程式生成主鍵。那麼Hibernate中也提供了相應的主鍵生成的方式,下面我們來看Hibernate的主鍵生成策略。

二、主鍵生成策略

2.1 主鍵的型別

  在講解Hibernate的主鍵生成策略之前,先來了解兩個概念,即自然主鍵和代理主鍵,具體如下:

  • 自然主鍵(少見):把具體業務含義的欄位作為主鍵,稱之為自然主鍵。例如在customer表中,如果把name欄位作為主鍵,其前提條件必須是:每一個客戶的名字不允許為null,不允許客戶重名,並且不允許修改客戶姓名。儘管這也是可行的,但是不能滿足不斷變化的業務需求,一旦出現了允許客戶重名的業務需求,就必須修改資料模型,重新定義表的主鍵,這給資料庫的維護增加了難度。
  • 代理主鍵:把不具備業務含義的欄位作為主鍵,稱之為代理主鍵。該欄位一般取名為”ID“,通常為整數型別,因為整數型別比字串型別要節省更多的資料庫空間。在上面的例子中,顯然更合理的方式是使用代理主鍵。

2.2 主鍵生成策略

  Hibernate中,提供了幾個內建的主鍵生成策略,其常用主鍵生成策略的名稱和描述如下:

  • 自然主鍵
    • assigned:自然主鍵生成策略. hibernate不會管理主鍵值.由開發人員自己錄入。如果不知道id元素的generator屬性,則預設使用該主鍵生成策略。

  • 代理主鍵
    • identity:主鍵自增.要求在資料庫中把主鍵定義為自增長型別.錄入時不需要指定主鍵.

    • sequence:Oracle中的主鍵生成策略.

    • increment(瞭解):主鍵自增.由hibernate來維護.每次插入前會先查詢表中id最大值.+1作為新主鍵值.只有當沒有其他程序向同一張表中插入資料時才可以使用,不能在叢集環境下使用。

    • native:hilo+sequence+identity 自動三選一策略.適合跨資料庫平臺開發

    • uuid:產生隨機字串作為主鍵. 主鍵型別必須為string 型別.

三、持久化物件的狀態

3.1 持久化物件三種狀態的概述

  Hibernate為了更好的來管理持久化類,將持久化類分成了三種狀態:瞬時態、持久態和脫管態,一個持久化類的例項可能處於三種不同狀態中的某一中。

  1、瞬時態(transient)

  瞬時態也稱為臨時態或者自由態,瞬時態的例項是由new命令建立、開闢記憶體空間的物件,不存在持久化標識OID(相當於主鍵值),尚未與Hibernate的Session關聯,在資料庫中也沒有記錄,失去引用後將被JVM回收。瞬時狀態的物件在記憶體中是孤立存在的,與資料庫的資料沒有任何關聯,僅是一個資訊攜帶的載體。

  2、持久態(persistent)

  持久態的物件存在持久化標識OID,加入到了Session快取中,並且相關聯的Session沒有關閉,在資料庫中有對應的記錄,每條記錄只對應唯一的持久化物件,需要注意的是,持久態物件是在事務還未提交前變成持久態的。

  3、脫管態(detached)

  脫管態也稱為離線態或者遊離態,當某個持久化狀態的例項與Session的關聯被關閉時就變成了脫管態。脫管態物件存在持久化標識OID,並且仍然與資料庫中的資料存在關聯,只是失去了與當前Session的關聯,脫管狀態物件發生改變時Hibernate不能檢測到。

3.2 區分物件的三種狀態

  @Test
    public void demo1() throws Exception {
        Session session = HibernateUtils.openSession();
        Transaction tx = session.beginTransaction();
        
        Customer customer = new Customer(); //瞬時態物件:沒有持久化標識OID,沒有被session管理
        customer.setCust_name("王五");
        session.save(customer); // 持久化物件:有持久化標識OID,被session管理
        
        tx.commit();
        session.close();
        
        System.out.println(customer); //脫管態物件:有持久化標識OID,沒有被session管理
    }

  customer物件由new關鍵字建立,此時還未與Session進行關聯,它的狀態稱為瞬時態;在執行了session.save(customer)操作後,customer物件納入了Session的管理範圍,這時的customer物件變成了持久態物件,此時Session的事務還沒有被提交;程式執行完commit()操作並關閉了Session後,customer物件與Session的關聯被關閉,此時customer物件就變成了脫管態。

3.3 三種狀態的轉換

     

  從圖中可以看出,當一個物件被執行new關鍵字建立後,該物件處於瞬時態;當對瞬時態物件執行Session的save()或saveOrUpdate()方法後,該物件將被放入Session的一級快取,物件進入持久態;當對持久態物件執行evict()、close()或clear()操作後,物件進入脫管態(遊離態);當直接執行Session的get()、load()、find()或iterate()等方法從資料庫裡查詢物件時,查詢到的物件也處於持久態;當對資料庫中的記錄進行update()、saveOrUpdate()以及lock()等操作後,此時脫管態的物件就過渡到持久態;由於瞬時態和脫管態的物件不在Session的管理範圍,所以一段時間後會被JVM回收。

  持久化物件的三種狀態可以通過呼叫Session中的一系列方法實現狀態間的轉換,具體如下:

【瞬時態轉換到其他狀態】

  通過前面學習可知,瞬時態的物件由new關鍵字建立,瞬時態物件轉換到其他狀態總結如下:

  • 瞬時態轉換為持久態:執行Session的save()或saveOrUpdate()方法。
  • 瞬時態轉換為脫管態:為瞬時態物件設定持久化標識OID。

  由於持久化物件狀態演化圖中沒有涉及到瞬時態轉換到脫管態的情況,這裡坐下簡要的說明,在前面的學習中可知,脫管態物件存在OID,但是沒有Session的關聯,也就是說脫管態和瞬時態的區別就是OID有沒有值,所以可以通過為瞬時態物件設定OID,使其變成脫管態物件。

Customer customer = new Customer(); // 瞬時態
customer.setCust_id(1); //脫管態

【持久態物件轉換到其他狀態】

  持久化物件可以直接通過Hibernate中Session的get()、load()方法,或者Query查詢從資料庫中獲得,持久態物件轉換到其他狀態總結如下: 

  • 持久態轉換為瞬時態:執行Session的delete()方法,需要注意的是被刪除的持久化物件,不建議再次使用。
  • 持久態轉換為脫管態:執行Session的evict()、close()或clear()方法。evict()方法用於清楚一級快取中某一物件;close()方法用於關閉Session,清楚一級快取;clear()方法用於清楚一級快取的所有物件。

【脫管態物件轉換到其他狀態】

  脫管態物件無法直接獲得,是由其他狀態物件轉換而來的,脫管態物件轉換到其他狀態總結如下:

  • 脫管態轉換為持久態:執行Session的update()、saveOrUpdate()或lock()方法。
  • 脫管態轉化為瞬時態:將脫管態物件的持久化標識OID設定為null。

  由於持久化物件狀態演化圖中沒有涉及到脫管態轉換到瞬時態的情況,這裡做下簡要的說明,跟瞬時態轉換到脫管態的情況相似,脫管態和瞬時態的區別就是OID有沒有值,所有可以通過將脫管態物件的OID設定為null,使其變成瞬時態物件。例如在session.close()操作後,加入程式碼customer.setCust_id(null),customer物件將由脫管態轉化為瞬時態。

3.4 持久態物件能夠自動更新資料庫

  // 測試持久化類的持久化物件有自動更新資料庫的能力
    @Test
    public void demo2() throws Exception {
        Session session = HibernateUtils.openSession();
        Transaction tx = session.beginTransaction();

        // 獲得持久化物件
        Customer customer = session.get(Customer.class, 1l);
        customer.setCust_name("王五");

        // session.update(customer); // 不用手動呼叫update方法j就可以更新

        tx.commit();
        session.close();
    }

  執行測試我們會發現,我們並沒有手動呼叫update方法,Hibernate就可以將資料自動更新了。持久態物件之所以有這樣的一個功能,其實都依賴了HIbernate的一級快取。

四、Hibernate的一級快取

  快取是計算機領域非常通用的概念。它介於應用程式和永久性資料儲存源(如硬碟上的檔案或者資料庫)之間,其作用是降低應用程式直接讀寫永久性資料儲存源的頻率,從而提高應用的執行效能。快取中的資料是資料儲存源中資料的拷貝。快取的物理介質通常是記憶體。

  Hibernate的快取分為一級快取和二級快取,Hibernate的這兩級快取都位於持久化層,儲存的都是資料庫資料的備份。其中第一級快取為Hibernate的內建快取,不能被解除安裝。接下來圍繞Hibernate的一級快取進行詳細的講解。

4.1 什麼是Hibernate的一級快取

  

  Hibernate的一級快取就是指Session快取,Session快取是一塊記憶體空間,用來存放相互管理的Java物件,在使用Hibernate查詢物件的時候,首先會使用物件屬性的OID值在Hibernate的一級快取中進行查詢,如果找到匹配OID值的物件,就直接將該物件從一級快取中取出使用,不會再查詢資料庫;如果沒有找到相同OID值的物件,則會去資料庫中查詢相應的資料。當從資料庫中查詢到所需資料時,該資料資訊也會放置到一級快取中。Hibernate一級快取的作用就是減少對資料庫的訪問次數

  在Session介面的實現中包含一系列的Java集合,這些Java集合構成了Session快取。只要Session例項沒有結束生命週期,存放在它快取中的物件也不會結束生命週期。所以一級快取也被稱為Session的基本快取。

  Hibernate的一級快取有如下特點:

  • 當應用程式呼叫Session介面的save()、update()、saveOrUpdate()時,如果Session快取中沒有相應的物件,Hibernate就會自動的把從資料庫中查詢到的相應物件資訊加入到一級快取中去。
  • 當呼叫Session介面的load()、get()方法,一級Query介面的list()、iterator()方法時,會判斷快取中是否存在該物件,有則返回,不會查詢資料庫,如果快取中沒有要查詢的物件,再去資料庫中查詢相應的物件,並新增到一級快取中。
  • 當呼叫Session的close()方法時,Session快取會被清空。

4.2 測試一級快取

  // 證明Hibernate一級快取的存在
    @Test
    public void demo3() throws Exception {
        Session session = HibernateUtils.openSession();
        Transaction tx = session.beginTransaction();

        // 馬上傳送一條sql語句查詢1號客戶,並將資料存入快取
        Customer customer1 = session.get(Customer.class, 1l);
        System.out.println(customer1);

        // 沒有傳送sql語句,從快取中取的資料
        Customer customer2 = session.get(Customer.class, 1l);
        System.out.println(customer2);

        // true 一級快取快取的是物件的地址
        System.out.println(customer1 == customer2);

        tx.commit();
        session.close();
    }

  以上程式碼中,第一次執行Session的get()方法獲取customer1物件時,由於一級快取中沒有資料,所以Hibernate會向資料庫傳送一條sql語句,查詢id為1的物件;當再次呼叫Session的get()方法獲取customer2物件時,不會再發送sql語句,這是因為customer2物件是從一級快取中獲取的。

  接下來驗證一下程式碼的執行結果是否和描述的一致。在Customer customer1 = session.get(Customer.class, 1l);這一行設定斷點,用debug方式執行該方法,程式進入斷點後點擊單步跳過(F6),程式碼執行過System.out.println(customer1);語句後,控制檯的輸出結果如下:

  

  從上圖的輸出結果中可以看出,第一次執行session.get()方法後,Hibernate向資料庫傳送了一條select語句,這說明此時customer1物件是從資料庫中查詢的。執行輸出語句資料customer1物件中的資料後,繼續執行程式,當執行到輸出customer2物件的程式碼處時,控制檯的輸出結果如下:

  

  從上圖的輸出結果可以看出,customer2物件的查詢結果被直接列印了,說明第二次呼叫Session物件的get()方法時,沒有向資料庫傳送select語句,而是直接從一級快取中獲取customer2物件。

  之前我們介紹過,Hibernate的持久態物件能夠自動更新資料庫,其實就是依賴了一級快取。那麼一級快取為什麼就可以去更新資料庫呢?其實是因為一級快取的一塊特殊區域——快照區。

4.3 一級快取的內部結構(快照區)

  HIbernate向一級快取放入資料時,同時複製一份資料放入到Hibernate的快照中,當使用commit()方法提交事務時,同時會清理Session的一級快取,這時會使用OID判斷一級快取中的物件和快照中的物件是否一致,如果這兩個物件中的屬性發送變化,則執行update語句,將快取中的內容同步到資料庫,並更新快照;如果一致,則不執行update語句。Hibernate快照的作用就是確保一級快取中的資料和資料庫中的資料一致。

// 一級快取中的快照區:持久態物件能夠自動更新資料庫
    @Test
    public void demo4() throws Exception {
        Session session = HibernateUtils.openSession();
        Transaction tx = session.beginTransaction();
        
        Customer customer = session.get(Customer.class, 1l);
        customer.setCust_name("張三");
        
        // 比對快取中和快照區的資料是否一致,如果一致,不更新資料庫
        // 如果不一致則自動更新資料庫
        tx.commit();
        session.close();
    }