1. 程式人生 > >(二)Hibernate事務以及一級快取

(二)Hibernate事務以及一級快取

一. Hibernate中的事務

1. 事務的回顧

1.1 什麼是事務(Transaction)(面試重點)

是併發控制的單元,是使用者定義的一個操作序列。這些操作要麼都做,要麼都不做,是一個不可分割的工作單位。通過事務,sql 能將邏輯相關的一組操作繫結在一起,以便伺服器 保持資料的完整性。事務通常是以begin transaction開始,以commit或rollback結束。Commint表示提交,即提交事務的所有操作。具體地說就是將事務中所有對資料的更新寫回到磁碟上的物理資料庫中去,事務正常結束。Rollback表示回滾,即在事務執行的過程中發生了某種故障,事務不能繼續進行,系統將事務中對資料庫的所有已完成的操作全部撤消,滾回到事務開始的狀態。

1.2 為什麼要使用事務?

  • 為了提高效能
  • 為了保持業務流程的完整性
  • 使用分散式事務

1.3 事務的特性

  • 原子性(atomicity)
    事務是資料庫的邏輯工作單位,而且是必須是原子工作單位,對於其資料修改,要麼全部執行,要麼全部不執行。

  • 一致性(consistency)
    事務在完成時,必須是所有的資料都保持一致狀態。在相關資料庫中,所有規則都必須應用於事務的修改,以保持所有資料的完整性。

  • 隔離性(isolation)
    一個事務的執行不能被其他事務所影響。企業級的資料庫每一秒鐘都可能應付成千上萬的併發訪問,因而帶來了併發控制的問題。由資料庫理論可知,由於併發訪問,在不可預料的時刻可能引發如下幾個可以預料的問題:
  • 永續性(durability)
    一個事務一旦提交,事物的操作便永久性的儲存在DB中。即使此時再執行回滾操作也不能撤消所做的更改

1.4 事務的併發問題

  • 髒讀(Dirty Read)

    一個事務讀取到了另一個事務未提交的資料操作結果。這是相當危險的,因為很可能所有的操作都被回滾。

    • 不可重複讀(虛讀)(NonRepeatable Read)

    一個事務對同一行資料重複讀取兩次,但是卻得到了不同的結果。例如事務T1讀取某一資料後,事務T2對其做了修改,當事務T1再次讀該資料時得到與前一次不同的值。

    • 幻讀(Phantom Read)

    事務在操作過程中進行兩次查詢,第二次查詢的結果包含了第一次查詢中未出現的資料或者缺少了第一次查詢中出現的資料,這是因為在兩次查詢過程中有另外一個事務插入資料造成的

1.5 事務的隔離級別

  • 1- 讀未提交

    Read uncommitted:最低級別,以上情況均無法保證。

  • 2- 讀已提交

Read committed:可避免髒讀情況發生。(Oracle預設)

  • 4- 可重複讀

Repeatable read:可避免髒讀、不可重複讀情況的發生。不可以避免虛讀。(MySQl預設)

  • 8- 序列化讀

Serializable:事務只能一個一個執行,避免了髒讀、不可重複讀、幻讀。執行效率慢,使用時慎重.

2.Hibernate的事務隔離級別

2.1 配置

Hibernate.cfg.xml中進行配置

<!-- 修復 hibernate 的隔離級別 -->
<property name="hibernate.connection.isolation">4</property>

可以配置四個值:

1: read uncommited

2: read commited

4: repeatable read

8: serializeable

3. 使用ThreadLocal管理Session(重點,記得會使用getCurrentSession)

3.1 事務管理案例

注意:下面的測試無法儲存資料,因為使用 openSession()方法拿到的都是獨立的session物件,事物中提交的session並不是dao中操作的session.
Java Dao程式碼:

package pojo.dao;
import org.hibernate.Session;
import pojo.Customer;
import tools.HibernateUtil;
public class CustomerDao {
    public void save(Customer cust){
        Session session = HibernateUtil.openSession();//每次都拿到新的session
        session.save(cust);
        //不能關閉session
        //session.close();
    }
}

JavaService程式碼:

package pojo.service;
import org.hibernate.Session;
import org.hibernate.Transaction;
import pojo.dao.CustomerDao;
import pojo.pojo.Customer;
import tools.HibernateUtil;
public class CustomerService {
    private CustomerDao dao = new CustomerDao();;

    public void save(Customer c1,Customer c2){
        Session session = HibernateUtil.openSession();
        //開啟事務
        Transaction tx = session.beginTransaction();
        try {
            dao.save(c1);
            dao.save(c2);
            tx.commit();
        } catch (Exception e) {
            e.printStackTrace();
            tx.rollback();
        }
    }
}

測試:

/**
     * 事務測試
     */
    @Test
    public void test2(){
        Customer c1 = new Customer();
        c1.setName("張三");

        Customer c2 = new Customer();
        c2.setName("李四");

        CustomerService service = new CustomerService();
        service.save(c1, c2);
    }
3.2 解決方案
3.2.1 修改session的獲取方式

將dao層和service層中需要用到session的地方使用getCurrentSession()
Session session = HibernateUtil.getCurrentSession();

3.2.2 在hibernate.cfg.xml中配置
<!-- 讓session被TheadLocal管理 -->
<property name="current_session_context_class">thread</property> 

注意

1.使用getCurrentSession時,增刪改查操作都需要事務支援

2.getCurrentSession建立的session會和繫結到當前執行緒,而openSession不會。

3.getCurrentSession建立的Session會在事務回滾或事物提交後自動關閉,而openSession必須手動關閉

二. 更新資料丟失

1. 什麼是更新資料丟失?
  • 如果不考慮隔離性,也會產生寫入資料的問題,這一類的問題叫丟失更新的問題。

    例如:兩個事務同時對某一條記錄做修改,就會引發丟失更新的問題。

        A事務和B事務同時獲取到一條資料,同時再做修改
    
        如果A事務修改完成後,提交了事務
    
        B事務修改完成後,不管是提交還是回滾,如果不做處理,都會對資料產生影響,如果回滾,
    

則B事務會回滾掉A事務提交的資料,如兩個同時更新! 第一次更新被第二次更新的覆蓋了!!

2. 更新資料丟失解決方案
  • 悲觀鎖:
    採用的是資料庫提供的一種鎖機制,如果採用做了這種機制,在SQL語句的後面新增 for update 子句
    當A事務在操作該條記錄時,會把該條記錄鎖起來,其他事務是不能操作這條記錄的。
    只有當A事務提交後,鎖釋放了,其他事務才能操作該條記錄

    實現程式碼:

session.get(Customer.class, 1,LockMode.UPGRADE);  //運算元第三個引數新增鎖
  • 樂觀鎖:
    採用版本號的機制來解決的。會給表結構新增一個欄位version=0,預設值是0
    當A事務在操作完該條記錄,提交事務時,會先檢查版本號,如果發生版本號的值相同時,才可以提交事務。同時會更新版本號version=1

    當B事務操作完該條記錄時,提交事務時,會先檢查版本號,如果發現版本不同時,程式會出現錯誤。

 1.在對應的JavaBean中新增一個屬性,名稱可以是任意的。
    例如:private Integer version; 提供get和set方法

  2.在對映的配置檔案中,提供<version name="version"/>標籤即可。   
    對比version 如果版本不是最新的 !那麼操作不成功!
    <!-- 就是實體實體類中version -->
    <version name="version"></version>

三. 持久化類講解

1. 什麼是持久化類?
  持久化類:是指其例項需要被 Hibernate 持久化到資料庫中的類。持久化類符合JavaBean的規範,包含一些屬性,以及與之對應的 getXXX() 和 setXXX() 方法。
2. 持久化類編寫規則
  1. get/set方法必須符合特定的命名規則,get 和set 後面緊跟屬性的名字,並且屬性名的首字母為大寫。
  2. name 屬性的 get 方法為 getName(),如果寫成 getname() 或 getNAME() 會導致 Hibernate 執行時丟擲以下異常:net.sf.hibernate.PropertyNotFoundException:Could not find a getter for porperty name in class mypack XXX
  3. 如果屬性為 boolean 型別,那麼 get 方法名即可以用 get 作為字首,也可以用 is 作為字首。
  4. 持久化類必須有一個主鍵屬性,用來唯一標識類的每一個物件。這個主鍵屬性被稱為物件標示符(OID,Object Identifier)。
  5. Hibernate要求持久化類必須提供一個不帶參的預設構造方法,在程式執行時,Hibernate 運用Java反射機制,呼叫java.Lang.raflect.Constructor.newInstance()方法來構造持久化類的例項。
  6. 使用非final類。在執行時生成代理是 Hibernate 的一個重要功能。如果持久化類沒有實現任何介面的話,Hibernate使用CGLIB生成代理,該代理物件時持久化類子類的例項。如果使用了final類,將無法生成CGLIB代理。還有一個可選的策略,讓 Hibernate 持久化類實現一個所有方法都宣告為public的介面,此時將使用JDK的動態代理。同時應該避免在非final類中宣告public final的方法。如果非要使用一個有public final的類,你必須通過設定lazy=”false“來明確地禁用代理
3. 自然和代理主鍵

持久化類中必須包含一個主鍵屬性,主鍵通常分為兩種,自然和代理!

  • 自然主鍵:物件本身的一個屬性.建立一個人員表,每個人都有一個身份證號.(唯一的)使用身份證號作為表的主鍵.自然主鍵.(開發中不會使用這種方式)
  • 代理主鍵:不是物件本身的一個屬性.建立一個人員表,為每個人員單獨建立一個欄位.用這個欄位作為主鍵.代理主鍵.(開發中推薦使用這種方式)
4. 主鍵生成策略(重點)
 hibernate框架可以有效的幫助我們生成資料主鍵,可以是自增長,也可以是UUID等模式!

修改生成策略位置:

<!-- 配置主鍵id
     name javaBean的屬性
     column 表結構的屬性
     如果相同可以去掉 column
-->
 <!-- 主鍵生成策略,修改class值即代表修改主鍵生成策略 -->
 <id name="cust_id" column="cust_id">
 <generator class="native"/>
 </id>

具體策略值:

  • increment:適用於short,int,long作為主鍵.不是使用的資料庫自動增長機制。
    Hibernate中提供的一種增長機制.
    先進行查詢 :select max(id) from user;
    再進行插入 :獲得最大值+1作為新的記錄的主鍵.
    問題:不能在叢集環境下或者有併發訪問的情況下使用.
  • identity:適用於short,int,long作為主鍵。但是這個必須使用在有自動增長資料庫中.採用的是資料庫底層的自動增長機制.底層使用的是資料庫的自動增長(auto_increment).像Oracle資料庫沒有自動增長.
    所以此值mysql支援!
  • sequence:適用於short,int,long作為主鍵.底層使用的是序列的增長方式.Oracle資料庫底層沒有自動增長,想自動增長需要使用序列.
    此值Oracle支援!
  • uuid:適用於char,varchar型別的作為主鍵.
    使用隨機的字串作為主鍵.
  • native:本地策略.根據底層的資料庫不同,自動選擇適用於該種資料庫的生成策略.(short,int,long)
    如果底層使用的MySQL資料庫:相當於identity.
    如果底層使用Oracle資料庫:相當於sequence.
  • assigned:主鍵的生成不用Hibernate管理了.必須手動設定主鍵.

  • 持久化物件的幾種狀態

  • 持久化物件狀態轉換

四. 持久化物件

1. 專案準備
1.1 建立專案 hibernate-02-optimize

1.2 匯入jar包

1.3 複製上個專案實體(客戶),對映,配置和工具類等!

額外新增一個使用者表,和實體類!

  • 建表語句
CREATE TABLE `user`(
   id integer primary key auto_increment,
   name varchar(10) not null,
   age integer,
   version integer
)
  • 建立實體類
public class User {
  private Integer id;
  private String name;
  private Integer age;
  private Integer version;
  //getter setter toString 
}
  • 建立持久化類對映檔案

    位置: 實體類相同資料夾

    命名: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>
    <!-- 配置類和表的對映  catalog="" 資料庫名稱-->
    <class name="pojo.User"  table="user" >

        <!-- 配置主鍵id 
             name javaBean的屬性
             column 表結構的屬性
             如果相同可以去掉 column
        -->
        <id name="id" column="id">
            <!-- 主鍵生成策略  遞增 -->
           <generator class="native"/>
        </id>
        <!-- 就是實體實體類中version -->
        <version name="version"></version>

        <!-- 其他的屬性 -->
        <property name="name" column="name" length="30"/>
        <property name="age" column="age"/>
    </class>
</hibernate-mapping>
  • 修改核心配置檔案,新增User的對映檔案
 <!-- 對映的 com開始-->
  <mapping resource="pojo/Customer.hbm.xml"/>
  <mapping resource="pojo/User.hbm.xml"/>
2. 持久化物件介紹

持久化類建立的物件就是持久化物件!

3. 持久化物件的三種狀態(重點)

Hibernate為了管理持久化物件:將持久化物件分成了三個狀態

  • 瞬時態:Transient Object
    沒有持久化標識OID, 沒有被納入到Session物件的管理.
  • 持久態:Persistent Object
    有持久化標識OID,已經被納入到Session物件的管理.
  • 脫管態(遊離態):Detached Object
    有持久化標識OID,沒有被納入到Session物件的管理.

持久化物件中,持久態最為重要,因為持久太物件具有自動更新功能!
展示持久化物件狀態:

@Test
public void  testStatus(){

   Session session = HibernateUtil.getSession();

   Transaction beginTransaction = session.beginTransaction();
    //持久化物件
    User user = new User();
    user.setName("王老五");
    user.setAge(36);  

    //----------- 以上是瞬時態 沒有session管理沒有 oid------------------
    //返回值就是生成的id
    Serializable id = session.save(user);
    System.out.println(id);
    beginTransaction.commit();

    //------------ 以上是持久態,有session管理,有oid-----------
    session.close();
System.out.println(user.getId());
    System.out.println(user.getName());
    //------------- 以上託管態, 有oid 但是沒有session管理!-----------   
}

測試自動更新功能:

@Test
   public void  testAuto(){

       Session session = HibernateUtil.getSession();

       Transaction beginTransaction = session.beginTransaction();

       User user = session.get(User.class, "8a8a200c5d7db0f7015d7db0fe280000");
       user.setName("修改的name");
       //看後臺輸出會發現,不用呼叫update方法,也會觸發sql語句修改使用者的name屬性!    
       beginTransaction.commit();
       session.close();
   }

自動更新功能,其實是藉助session的一級快取!一級快取後面進行講解!

3. 持久化物件狀態轉換
這裡寫圖片描述
  1. 瞬時態 – 沒有持久化標識OID, 沒有被納入到Session物件的管理
    獲得瞬時態的物件
    User user = new User()
    • 瞬時態物件轉換持久態
      • save()/saveOrUpdate();
    • 瞬時態物件轉換成脫管態
      • user.setId(1)
  2. 持久態– 有持久化標識OID,已經被納入到Session物件的管理
    獲得持久態的物件
    get()/load();
    • 持久態轉換成瞬時態物件
      • delete(); — 比較有爭議的,進入特殊的狀態(刪除態:Hibernate中不建議使用的)
    • 持久態物件轉成脫管態物件
      • session的close()/evict()/clear();
  3. 脫管態– 有持久化標識OID,沒有被納入到Session物件的管理

        獲得託管態物件:不建議直接獲得脫管態的物件.
    
           User user = new User();
    
           user.setId(1);
    
        脫管態物件轉換成持久態物件
    
            update();/saveOrUpdate()/lock();
    
        脫管態物件轉換成瞬時態物件
    
            user.setId(null);
    

五. Hibernate的一級快取(重點)

1. 一級快取介紹
Hibernate的一級快取是指Session(屬於事務範圍的快取,由Hibernate管理,無需干預),它是一塊記憶體空間,用來存放從資料庫查詢出的java物件,有了一級快取,應用程式可以減少訪問資料庫的次數,提高了效能。

在使用Hibernate查詢物件的時候,首先會使用物件屬性的OID值(對應表中的主鍵)在Hibernate的一級快取進行查詢,如果找到,則取出返回,不會再查詢資料庫,如果沒有找到,再到資料庫中進行查詢操作。然後將查詢結果存放到Session一級快取中。
這裡寫圖片描述

  • 一級快取演示
package pojo.test;

import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Test;

import pojo.Customer;
import tools.HibernateUtil;

public class CacheLevelOneTest {

    /**
     * 使用程式碼來證明Hibernate的一級快取是存在的!
     */
    @Test
    public void testCache(){

        Session session = HibernateUtil.openSession();
        Transaction tx = session.beginTransaction();

        //第1次查詢
        Customer c1 = session.get(Customer.class, 1L);
        System.out.println(c1);

        //第2次查詢
        Customer c2 = session.get(Customer.class,1L);
        System.out.println(c2);
        //第二次查詢不觸發sql語句,直接獲取快取中的結果!
        tx.commit();
        session.close();    
    }
}
2. Hibernate 的快照機制

當執行 commit() 時,Hibernate同時會執行 flush() 方法,hibernate會清理session的一級快取(flush),也就是將堆記憶體中的資料與快照中的資料進行對比,如果不一致,則會執行同步(update)操作,若相同,則不執行update。

1、快照是資料的副本

2、快照屬於一級快取

3、快照是在堆記憶體中的

4、快照的作用:保證資料一致性

/**
 * 說明持久態物件可以直接更新資料庫的資料!
 */
@Test
public void testAutoUpdate(){

    Session session = HibernateUtil.openSession();
    Transaction tx = session.beginTransaction();

    //獲取到一個持久態物件
    Customer cust = session.get(Customer.class, 1L);
    //修改cust的資料
    cust.setName("湯姆");

    //沒有 必要執行update語句,因為現在持久態物件已經能夠更新資料庫的資料啦!
    //session.update(cust);

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

}
3. 一級快取管理

Q:如果持久態物件不在一級快取中,可以更新資料庫嗎?

A:不能!

把物件移出一級快取的方法:

session.evict(object) : 把一個物件移出一級快取

session.clear() : 把一級快取的所有物件移出

測試:以下測試資料不會被更新

/**
  * 一級快取的管理
  */
@Test
public void testEvictAndClear(){        
  Session session = HibernateUtil.openSession();
  Transaction tx = session.beginTransaction();  
  Customer cust = session.get(Customer.class, 1L); //cust是持久態物件,在一級快取
  cust.setName("老王");

  //把cust物件移出一級快取
  session.evict(cust);

  //清空一級快取
  //session.clear();

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