Hibernate 基本操作、懶載入以及快取

god23bin發表於2023-05-17

前言

上一篇我們們介紹了 Hibernate 以及寫了一個 Hibernate 的工具類,快速入門體驗了一波 Hibernate 的使用,我們只需透過 Session 物件就能實現資料庫的操作了。

現在,這篇介紹使用 Hibernate 進行基本的 CRUD、懶載入以及快取的知識。

提示:如果你還沒看上一篇,那麼建議你看完上一篇再來看這篇。

上一篇:一文快速入門體驗 Hibernate

基本的 CRUD

以下程式碼均寫在測試類 HibernateTest 中

插入操作

這個在上一篇已經演示過,這裡便不再演示。

查詢操作

查詢有 2 種方式,透過 Session 物件的 get 方法 或者 load 方法來實現查詢,主要將查詢的資料結果封裝到一個 Java 物件中。

  1. get 方法
    @Test
    public void queryByGet() {
        // 獲取 Session 物件
        Session session = HibernateUtil.getSession();
        try {
            // 使用 get() 方法,第一個引數是持久化類的型別引數,第二個引數是主鍵標識引數,如果沒有匹配的記錄,那麼會返回 null
            User user = session.get(User.class, new Integer("1"));
            System.out.println("使用者ID:" + user.getId());
        } catch (Exception e) {
            System.out.println("查詢User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:可以看到,執行了查詢 SQL,並列印了使用者 ID。

INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
五月 08, 2023 11:38:59 下午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
使用者ID:1
  1. load 方法
    @Test
    public void queryByLoad() {
        // 獲取 Session 物件
        Session session = HibernateUtil.getSession();
        try {
            // 使用 load() 方法,它返回物件的代理,只有該代理被呼叫時,Hibernate 才會真正去執行 SQL 查詢
            User user = session.load(User.class, new Integer("1"));
            // ID 是已知的,不用進行查詢
            System.out.println("使用者ID:" + user.getId());
            // 此時該代理被呼叫,就執行 SQL 語句,得到真正的資料記錄
            System.out.println("使用者名稱稱:" + user.getName());
        } catch (Exception e) {
            System.out.println("查詢User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:

五月 08, 2023 11:40:13 下午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
五月 08, 2023 11:40:14 下午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
使用者ID:1
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
使用者名稱稱:god23bin

可以看到,是先列印使用者ID的,這裡還沒有執行查詢 SQL,直到下一條語句中的 user.getName() 的執行,查詢的 SQL 語句才被 Hibernate 執行

修改操作

想對某條資料進行修改操作,那麼需要將它先查詢出來,然後進行修改。這裡就執行了兩條 SQL,保險起見,開啟事務,然後執行這兩條 SQL,接著提交事務。當然,這兩條 SQL,Hibernate 幫我們寫的啦!

    @Test
    public void update() {
        // 獲取 Session 物件
        Session session = HibernateUtil.getSession();
        try {
            // 開啟事務
            session.beginTransaction();
            // 進行查詢,將結果封裝成 user 物件
            User user = session.get(User.class, new Integer("1"));
            // 對 user 物件進行修改
            user.setName("公眾號:god23bin");
            user.setPassword("456789");
            // 提交事務
            session.getTransaction().commit();
        } catch (Exception e) {
            // 發生異常,則回滾事務
            session.getTransaction().rollback();
            System.out.println("修改User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:

五月 09, 2023 12:00:16 上午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
五月 09, 2023 12:00:17 上午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
Hibernate: 
    update
        user 
    set
        name=?,
        password=? 
    where
        id=?
        

可以看到執行前和執行後,資料的變化,如圖:

image-20230509000227733

如果螢幕前的小夥伴是按照我的步驟一步一步跟下來,那麼你可能會遇到中文亂碼的問題,此時需要在 hibernate.cfg.xml 配置檔案中修改 URL,加上兩個引數 useUnicode=true&characterEncoding=UTF-8,如下:

<property name="connection.url">jdbc:mysql://localhost:3306/demo_hibernate?useUnicode=true&amp;characterEncoding=UTF-8</property>

刪除操作

刪除操作需要先把資料查詢出來,然後透過 Session 物件的 delete 方法將其刪除。程式碼如下:

    @Test
    public void delete() {
        // 獲取 Session 物件
        Session session = HibernateUtil.getSession();
        try {
            session.beginTransaction();
            User user = session.get(User.class, new Integer("1"));
            // 刪除操作
            session.delete(user);
            session.getTransaction().commit();
        } catch (Exception e) {
            session.getTransaction().rollback();
            System.out.println("刪除User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:

五月 09, 2023 12:10:09 上午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
五月 09, 2023 12:10:10 上午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
Hibernate: 
    delete 
    from
        user 
    where
        id=?

關於 Hibernate 中物件的狀態

在Hibernate中,物件的狀態有 4 種,分別為 Transient、Persistent、Detached、Removed,譯名就比較多,方便起見,我選擇 3 個字的譯名:

  1. 瞬時態(Transient):當一個物件被例項化後,它處於瞬時態,簡單理解,就是 new 操作之後。瞬時態的物件沒有與之關聯的資料庫記錄,並且沒有被 Hibernate 的 Session 管理。當將瞬時態的物件關聯到持久態物件或透過 Session 物件的 savepersist 等方法進行持久化操作後,該物件的狀態會發生變化,轉成持久態。
  2. 持久態(Persistent):當一個物件與 Hibernate 的 Session 關聯後,它就處於持久態。持久態的物件有與之對應的資料庫記錄,並且被 Hibernate 的 Session 管理。對持久態物件的任何更改都會自動同步到資料庫。持久態物件可以透過Session的 getload 等方法從資料庫中獲取,或者透過 saveupdatepersist 等方法進行持久化操作。
  3. 遊離態(Detached):當一個持久態物件與 Hibernate 的 Session 分離後,它處於遊離態。遊離態的物件仍然有與之對應的資料庫記錄,但不再受 Hibernate 的 Session 管理。對遊離態物件的更改不會自動同步到資料庫。可以透過 Session 的 evictclear 等方法將持久態物件轉變為遊離態物件,或者透過 Session 的 merge 方法將遊離態物件重新關聯到 Session 中。
  4. 刪除態(Removed):當一個持久態物件被從 Hibernate 的 Session中刪除後,它處於刪除態。刪除態的物件仍然有與之對應的資料庫記錄,但即將被從資料庫中刪除。刪除態物件可以透過 Session 的delete 方法進行刪除操作。

Hibernate 透過跟蹤物件的狀態變化,實現了物件與資料庫的同步。在 Hibernate 的事務管理中,物件的狀態轉換是自動進行的,我們無需手動操作,Hibernate 會根據物件的狀態進行相應的資料庫操作,保證物件與資料庫的一致性。

需要注意的是,Hibernate 的物件狀態與資料庫的操作並不是一一對應的,Hibernate 提供了一系列的持久化方法和操作,我們可以根據具體的需求選擇合適的方法來進行物件狀態的轉換和資料庫操作。對於複雜的業務邏輯和資料處理,需要仔細理解和管理物件的狀態,以避免資料不一致的問題。

懶載入

Hibernate 的懶載入(Lazy Loading)是一種延遲載入策略,它允許程式在需要訪問相關資料時才從資料庫中載入關聯物件的屬性或集合。

在 Hibernate 中,懶載入是透過使用代理物件來實現的。實際上,我們在演示 Session 物件的 load() 方法時,就是懶載入了,一開始返回的是代理物件,並沒有直接查詢資料庫,而是直到該代理物件的屬性或方法被呼叫時,Hibernate 會根據需要自動執行額外的資料庫查詢,從而延遲載入關聯的資料。

這就是懶載入,等到需要的時候才去載入。

懶載入的主要優點是可以提高系統效能和減少不必要的資料庫查詢。如果一個物件關聯的屬性或集合在業務邏輯中很少被使用,懶載入可以避免不必要的資料庫訪問,減輕資料庫負載。

除了 load 方法實現的懶載入,我們還可以透過設定對映檔案中的 <property> 標籤的 lazy 屬性實現懶載入:

<property name="name" type="string" lazy="true" /> <!-- name 屬性被設定成懶載入-->

快取

快取是一種臨時儲存資料的方式,將資料儲存在更快速的儲存介質(如記憶體)中,以便將來能夠快速訪問和檢索。

Hibernate 提供了快取的技術,主要用於儲存實體物件以及查詢的結果集。快取分為一級快取(Session 快取)和二級快取(Session Factory 快取)

一級快取

一級快取是與 Session 相關聯的快取,它儲存了從資料庫中讀取的實體物件。在同一個 Session 中,當多次查詢相同的資料時,Session 首先會根據對應的持久化類和唯一性標識(一般指的是ID)去快取中查詢是否存在該資料。如果存在,則直接從快取中獲取,而不再訪問資料庫;如果不存在,則繼續向二級快取種查詢。

一級快取是預設開啟的,可以提高讀取效能。

示例:

    @Test
    public void testFirstLevelCache() {
        Session session = HibernateUtil.getSession();
        try {
            System.out.println("第一次查詢:");
            User user = session.get(User.class, new Integer("2"));
            System.out.println("使用者名稱:" + user.getName());
            
            System.out.println("第二次查詢:");
            User user2 = session.get(User.class, new Integer("2"));
            System.out.println("使用者名稱:" + user2.getName());
        } catch (Exception e) {
            System.out.println("查詢User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:

五月 09, 2023 9:35:31 下午 org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
五月 09, 2023 9:35:32 下午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
第一次查詢:
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
使用者名稱:god23bin
第二次查詢:
使用者名稱:god23bin

可以看到,第二次查詢是沒有執行 SQL 的,直接從一級快取中獲取。

二級快取

二級快取是在 SessionFactory 級別上的快取,用於快取多個 Session 之間共享的資料。它可以減少對資料庫的訪問次數,提高效能和擴充套件性。二級快取可以儲存實體物件、集合物件以及查詢結果集。

由於 Hibernate 本身並未提供二級快取的具體實現,所以需要藉助其他快取外掛或者說策略來實現二級快取。比如 Ehcache、Redis 等。

我們這裡直接使用 Ehcache。

  1. 引入依賴項
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.6.14.Final</version>
</dependency>

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.0</version>
</dependency>
  1. 開啟二級快取

二級快取預設是關閉的,我們需要手動開啟。在 hibernate.cfg.xml 中開啟二級快取:

<?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
                "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        ...
        <!-- 開啟二級快取 -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <!-- 開啟查詢快取 -->
        <property name="hibernate.cache.use_query_cache">true</property>
        <!-- 指定使用的快取實現類 -->
        <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.internal.SingletonEhcacheRegionFactory</property>
    </session-factory>
</hibernate-configuration>
  1. 建立快取配置檔案

我們在 /src/main/resources 目錄下建立快取配置檔案 ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">

    <!-- 硬碟儲存:將快取中暫時不使用的物件,持久化到硬碟 -->
    <!-- path 屬性:指定在硬碟上儲存物件的路徑 -->
    <!-- java.io.tmpdir 是預設的臨時檔案路徑。 可以透過右邊的方式列印出具體的檔案路徑: System.out.println(System.getProperty("java.io.tmpdir")); -->
    <diskStore path="java.io.tmpdir"/>


    <!-- defaultCache:預設的快取策略 -->
    <!-- eternal 屬性:設定快取的elements是否永遠不過期。如果為true,則快取的資料始終有效,如果為false那麼還要根據timeToIdleSeconds,timeToLiveSeconds判斷 -->
    <!-- maxElementsInMemory 屬性:在記憶體中快取的element的最大數目 -->
    <!-- overflowToDisk 屬性:如果記憶體中資料超過記憶體限制,是否要快取到硬碟上 -->
    <!-- diskPersistent 屬性:是否在硬碟上持久化。指重啟 JVM 後,資料是否有效。預設為false -->
    <!-- timeToIdleSeconds 屬性:物件空閒時間(單位:秒),指物件在多長時間沒有被訪問就會失效。只對eternal為false的有效。預設值0,表示一直可以訪問 -->
    <!-- timeToLiveSeconds 屬性:物件存活時間(單位:秒),指物件從建立到失效所需要的時間。只對eternal為false的有效。預設值0,表示一直可以訪問 -->
    <!-- memoryStoreEvictionPolicy 屬性:快取的 3 種清除策略,因為快取區域是一定的,滿了之後就需要清除不需要的資料 -->
    <!-- 1. FIFO:first in first out (先進先出). 先快取的資料會先被清除-->
    <!-- 2. LFU:Less Frequently Used (最少使用).意思是一直以來最少被使用的。快取的元素有一個 hit 屬性,hit 值最小的將會被清除 -->
    <!-- 3. LRU:Least Recently Used(最近最少使用). (ehcache 預設值).快取的元素有一個時間戳,當快取容量滿了,而又需要騰出地方來快取新的元素的時候,那麼現有快取元素中時間戳離當前時間最遠的元素將被清除 -->

    <defaultCache eternal="false"
                  maxElementsInMemory="1000"
                  overflowToDisk="false"
                  diskPersistent="false"
                  timeToIdleSeconds="0"
                  timeToLiveSeconds="600"
                  memoryStoreEvictionPolicy="LRU"/>

    <!-- name: Cache的名稱,必須是唯一的(ehcache會把這個cache放到HashMap裡)-->
    <cache name="userCache"
           eternal="false"
           maxElementsInMemory="100"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="300"
           memoryStoreEvictionPolicy="LRU"/>
</ehcache>
  1. 在持久化類的對映檔案中指定快取策略

User.hbm.xml:

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="cn.god23bin.demo.domain.entity.User" table="user">
        <!-- 指定快取策略 -->
        <cache usage="read-only"/>
        ...
    </class>
</hibernate-mapping>

測試二級快取:

    @Test
    public void testSecondLevelCache() {
        Session session1 = HibernateUtil.getSession();
        Session session2 = HibernateUtil.getSession();
        try {
            System.out.println("第一個 Session 去查詢資料並封裝成物件");
            User user1 = session1.get(User.class, new Integer("2"));
            System.out.println("使用者名稱:" + user1.getName());

            System.out.println("第二個 Session 去查詢同一資料並封裝成物件");
            User user2 = session2.get(User.class, new Integer("2"));
            System.out.println("使用者名稱:" + user1.getName());
        } catch (Exception e) {
            System.out.println("查詢User資料失敗!");
            e.printStackTrace();
        } finally{
            // 關閉 Session 物件
            HibernateUtil.closeSession();
        }
    }

控制檯輸出:

五月 09, 2023 11:18:31 下午 org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator initiateService
INFO: HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
第一個 Session 去查詢資料並封裝成物件
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.name as name2_0_0_,
        user0_.password as password3_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
使用者名稱:god23bin
第二個 Session 去查詢同一資料並封裝成物件
使用者名稱:god23bin

總結

本篇文章主要講了基本的 CRUD 操作,都是透過 Session 去操作的,根據一個持久化類的型別以及一個唯一標識進行相關操作,然後講了 Hibernate 中的物件的狀態,有 4 種,分別是瞬時、持久、遊離、刪除。

接著說了 Hibernate 的懶載入,有利於降低資料庫的開銷,當然快取也是,除了加快我們的訪問速度,也降低了直接訪問資料庫的開銷,快取就兩種,一級和二級,一級預設是開啟的,二級需要引入相關的依賴項,然後進行配置,開啟二級快取,配置快取策略。

這裡附上整個專案的目錄結構,便於對照:

image-20230509234027184

以上,就是本篇的內容,這些都應該掌握。我們們下期再見。

最後的最後

希望各位螢幕前的靚仔靚女們給個三連!你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!

我們們下期再見!

相關文章