事務、DAO、連接池、Dbutils工具包
尚硅谷JDBC筆記-02

事務Transaction

Transaction,台灣:交易,中國:事務

  • 在MySQL中只有使用了Innodb資料庫引擎的資料庫或表才支援事務

  • 事務 = 一組操作資料庫的動作集合,使資料從一種狀態變換為另一種狀態

  • 一個事務保證一組SQL語句要嘛全部執行,要嘛全部不執行

    • 要嘛提交Commit,要嘛回滾Rollback
  • 數據一旦提交,就不可回滾

  • 那些操作會自動提交?

    • DDL(刪庫建表之類的操作):必定自動提交
    • DML(操作某筆資料):預設會自動提交,但可以set autocommit = false
    • 關閉連接時,預設也會自動提交

事務的相關方法

java.sql.Connection包提供,顯然這些方法都是由Connection物件調用

public void setAutoCommit(boolean)
public boolean getAutoCommit()
public void commit()
public void rollback()

事務實作流程

  1. 關閉自動提交
  2. 執行事務語句,可能有多行
  3. commit()手動提交,並還原自動提交設定
  4. catch (Exception e) 區塊使用rollback()回滾
// 範例
public String delete(String id) {
    String ID = id;
    db = new getConnection();
   Connection con = db.getConnection();
   try {
        con.setAutoCommit(false);
        db.executeUpdate("delete from helloworld where ID=" + ID); //更新操作1
       db.executeUpdate("delete from helloworld _book where ID=" + ID); //更新操作2
       db.executeUpdate("delete from helloworld_user where ID=" + ID); //更新操作3
       con.commit();//提交JDBC事務
       con.setAutoCommit(true);
        db.close();
        return "success";
    }
    catch (Exception e) {
      con.rollBack();//回滾JDBC事務
      e.printStackTrace();
       db.close();
       return "fail";
   }
}

事務的ACID

  • 原子性(Atomicity):一個事務(transaction)中的所有操作不可分割,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣
  • 一致性(Consistency):在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。
  • 隔離性(Isolation):資料庫允許多個併發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料干擾。事務隔離分為不同級別,下述
  • 持久性性(Durability):交易處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

隔離性

資料庫的併發問題

  • 髒讀(dirty read):A讀取了已經被B更新但未提交的資料,之後B回滾,導致A讀取了無效的"髒"資料
  • 不可重複讀(non-repeatable read):A與B讀取了資料,之後B更新了資料,導致A再讀取時前後資料不同
  • 幻讀(phantom read):A從一張表讀取了某些資料,B在該表插入了一些新的行,導致A再讀取表時前後資料量不同
隔離等級

隔離等級由低至高,越下面越安全,效率也越低

  • 讀未提交(Read uncommitted):允許讀取其他事務未提交的變更,3種問題都會有
  • 讀已提交(Read committed):只允許讀取其他事務已提交的變更,可避免髒讀
  • 可重複讀(Repeatable read):確保一個事務多次讀取一個字段都是相同的值,在此事務持續時間內,禁止其他事務對這個字段進行更新,可避免髒讀與不可重複讀
  • 序列化(Serializable):確保一個事務從一個表中讀取到相同的行,在此事務持續時間內,禁止其他事務對整張表進行插入、更新與刪除,可避免所有問題,但效率很低
資料庫差異
  • Oracle支援2種事務隔離級別:Read committed與Serializable
    • Oracle預設的事務隔離級別為:Read committed
  • MySQL支援 4 種事務隔離級別
    • MySQL預設的事務隔離級別為:Repeatable read
MySQL設定隔離
  • 每啟動一個mysql程序,就會獲得一個單獨的資料庫連接,每個資料庫連接都有一個全域變數 @@tx_isolation表示當前的事務隔離級別

    • mysql8為@@transaction_isolation
  • 查看當前的隔離級別:

SELECT @@tx_isolation;
  • 設置當前MySQL連接的隔離級別:
set  transaction isolation level read committed;
  • 設置資料庫系統的全域的隔離級別:
set global transaction isolation level read committed;
  • 補充操作,創建MySQL資料庫使用者
create user tom identified by 'abc123';
  • 授予許可權
#授予通過網路方式登錄的tom使用者,對所有庫所有表的全部許可權,密碼設為abc123.
grant all privileges on *.* to tom@'%'  identified by 'abc123';

#給tom使用者使用本地命令列方式,授予atguigudb這個庫下的所有表的插刪改查的許可權。
grant select,insert,delete,update on atguigudb.* to tom@localhost identified by 'abc123';

DAO

https://www.bilibili.com/video/BV1eJ411c7rf?p=45

他這邊講這個相當於自己造輪子,以後框架用到的時候矇了可以再回來看

DataAccessObjects資料存取物件,專門用來跟資料庫打交道,位於業務邏輯與資料之間的橋樑,確切來說就是把獲取/關閉連接與增刪改查封裝起來

  • DAO介面:把對資料庫的所有操作定義成抽象方法,可以提供多種實現
  • DAO 實現類:針對不同資料庫給出DAO介面定義方法的具體實現
  • 實體類:用於存放與傳輸物件資料,比如表示查詢到的所有欄位存放的的物件
  • 資料庫連接和關閉工具類:避免了資料庫連接和關閉代碼的重複使用,方便修改

同場加映JavaBean

JavaBean是一種public class,目的是為了方便在程式中被重複使用,類似容器的概念,我不在乎你裡面封裝了啥,只要滿足特定條件大家都可以很方便地拿來用。JavaBean需滿足這4點約定:

  • 所有屬性為private
  • 提供預設空參的構造方法
  • 提供public的getter和setter
  • 實現serializable介面,可序列化保證可以跨平台溝通

連接池

即為資料庫連接建立緩衝池

  • 想像有一口井,以前誰要用水就自己去打一桶水,多少人取水就要多少條連接
  • 連接池就像在井口邊放一個大水缸,雇一位專業打水人負責井中取水事務,當其他人要用水只需要從水缸取即可,方便又快速

優勢

  • 資源復用:不需要頻繁創立新的連接
  • 提高速度:由連接池完成連接資料庫的初始化,誰要用只要來連池子,省時
  • 資源分配:由連接池管理,可以限制最大連接數量等等
  • 防止洩漏:其實也是管理,比如有人用到超時就把它收回

實現

  • JDBC的資料庫連接池使用javax.sql.DataSource接口來表示,此接口通常由WebServer實現,也有許多開源的例如:

    • proxool:更新時間截止2008年。速度可以,穩定性稍差,高併發的情況下會出錯
    • c3p0:太古老,代碼及其複雜,不利於維護
    • dbcp:是 apache上的一個 java 連接池項目,也是 tomcat 使用的連接池元件
    • BoneCP:13年前最快的連接池項目。2013年後不再更新,心灰意冷
    • druid:是alibba出品的一個功能比較全面,且擴展性較好的資料庫連接池,比較方便對jdbc介面進行監控跟蹤等
    • HikariCP:光連接池,目前被SpringBoot2官方推薦使用的資料庫連接池。
  • 目前2021年的時間點偏向用HikariCP或druid

  • DataSource通常被稱為資料來源,它包含連接池和連接池管理兩個部分,習慣上也經常把DataSource稱為連接池

  • DataSource用來取代DriverManager來獲取Connection,獲取速度快,同時可以大幅度提高資料庫存取速度

  • 特別注意:

    • 資料來源和資料庫連接不同,資料來源無需創建多個,它是產生資料庫連接的工廠,因此整個應用只需要一個資料來源即可
    • 當資料庫訪問結束後,程式還是像以前一樣關閉資料庫連接conn.close() 它並沒有關閉資料庫的物理連接,它僅僅把資料庫連接釋放,歸還給了資料庫連接池

使用

即使不同家提供的連接池,流程也都大同小異

  1. 下載驅動,導入jar包

    • https://github.com/alibaba/druid,這裡用德魯伊示範
    • 阿帕契也有一個項目叫Apache Druid,那是一個資料儲存分析系統,兩者差很遠別搞混了
  2. 編輯設定檔xxx.properties,設定資料庫網址、帳號密碼,連接數…等等

  3. 造流,導入properties

  4. 用提供的靜態方法建立DataSource物件

  5. DataSource物件getConnection()

// 範例,使用druid連接池
Properties properties = new Properties();
properties.load(getClass().getClassLoader().getResourceAsStream("druid.properties"));
// 或是直接類名.class.getClassLoader()...
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
Connection conn = dataSource.getConnection();
System.out.println(conn);
  • 路徑src下的設定檔druid.properties
url=jdbc:mysql://localhost:3306/jdbc_learn?rewriteBatchedStatements=true
username=root
password=1234
driverClassName=com.mysql.cj.jdbc.Driver
initialSize=10
maxActive=20
maxWait=1000
filters=wall

Dbutils工具包

了解底層原理後,實際開發當然是拿別人造好的輪子來用

  • https://commons.apache.org/proper/commons-dbutils/
  • Apache提供的開源 JDBC工具類庫,它是對JDBC的簡單封裝,學習成本極低,能極大簡化jdbc編碼的工作量,同時也不會影響程式的性能,包含3個主要API:
    • org.apache.commons.dbutils.QueryRunner
    • org.apache.commons.dbutils.ResultSetHandler
    • org.apache.commons.dbutils.DbUtils

QueryRunner

用於增刪改查

  • update:可以是增刪改
  • insert:只能用於插入
  • batch:批次處理
  • query:查詢,返回ResultSet,並且會自行管理用到的PreparedStatementResultSet資源
// 範例
@Test
public void t03() throws Exception {
    // 連接池
    Properties properties = new Properties();
    properties.load(getClass().getClassLoader().getResourceAsStream("druid.properties"));
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
    Connection conn = dataSource.getConnection();

    // 使用空參構造器
    String sql = "update customers set  name = ? where id=?";
    QueryRunner qr = new QueryRunner();
    qr.update(conn, sql, "BBB", 2);

    // 使用dataSource構造
    QueryRunner qr2 = new QueryRunner(dataSource);
    qr2.update("update customers set  name = ? where id=?", "CCC", 3);

}
  • 創建QueryRunner物件時,構造器有沒有帶連接會決定執行操作(例如update)時第一個參數要不要傳入連接,這個差異不光在於省一個要寫的參數
  • 假如我要提交一個事務,想當然我需要在同一條連接完成,如果發生錯誤就可以整串回滾

ResultSetHandler

更好處理ResultSet的中轉類

查詢時會得到java.sql.ResultSet類物件,我們需要提早造一個Handler類去處理,可選擇的如下:

  • ArrayHandler:把結果集中的第一行資料轉成物件陣列。
    • ArrayListHandler:把結果集中的每一行資料都轉成一個陣列,再存放到List中。
  • BeanHandler:將結果集中的第一行資料封裝到一個對應的JavaBean實例中。
    • BeanListHandler:將結果集中的每一行資料都封裝到一個對應的JavaBean實例中,存放到List裡。
  • MapHandler:將結果集中的第一行資料封裝到一個Map裡,key是列名,value就是對應的值。
    • MapListHandler:將結果集中的每一行資料都封裝到一個Map裡,然後再存放到List
  • ColumnListHandler:將結果集中某一列的資料存放到List中。
  • KeyedHandler(name):將結果集中的每一行資料都封裝到一個Map裡,再把這些map再存到一個map裡,其key為指定的key。
  • ScalarHandler:查詢單個值對象
// 一樣先造一個QueryRunner
QueryRunner qr2 = new QueryRunner(dataSource);

// 造一個用來接收的Handler,類來自Customers的javabean
// 注意new的右邊()需要填入XXX.class
BeanHandler<Customers> cbh = new BeanHandler<>(Customers.class);
Customers queryCus = qr2.query("select * from customers where id = ?", cbh, 2);
System.out.println(queryCus);

// 接收N筆資料
BeanListHandler<Customers> cusListHandler = new BeanListHandler<>(Customers.class);
List<Customers> cusList = qr2.query("select * from customers", cusListHandler);
cusList.forEach(System.out::println);

// 將查來的一個結果直接裝成一個map
MapHandler mapHandler = new MapHandler();
Map<String, Object> objectMap = qr2.query("select * from customers where id = ?", mapHandler, 6);
System.out.println(objectMap);

// 將查來的多告結果裝成一個mapList ***重點用法***
MapListHandler mapListHandler = new MapListHandler();
List<Map<String, Object>> queryList = qr2.query("select * from customers where id <= ?", mapListHandler, 10);
queryList.forEach(System.out::println);

// scalarHandler查詢一個值
ScalarHandler scalarHandler = new ScalarHandler();
long count = (long) qr2.query("select count(*) from customers", scalarHandler);
System.out.println(count);

DbUtils

用於管理連接本身,所有方法都是靜態,最常就是用來關閉連接

  • public static void close(…) throws java.sql.SQLException:省去檢查null,如果不是的話,它們就關閉Connection、Statement和ResultSet
  • public static void closeQuietly(…):省掉檢查null跟拋異常
  • public static void commitAndClose(Connection conn)throws SQLException: 用來提交連接的事務,然後關閉連接
  • public static void commitAndCloseQuietly(Connection conn)
  • public static void rollback(Connection conn)throws SQLException:允許conn為null,因為方法內部做了判斷
  • public static void rollbackAndClose(Connection conn)throws SQLException
  • rollbackAndCloseQuietly(Connection)
  • public static boolean loadDriver(java.lang.String driverClassName):裝載並註冊JDBC驅動程式,如果成功就返回true。使用該方法,你不需要捕捉這個異常ClassNotFoundException

小結

  • 事務:關閉自動提交、catch區塊宣告回滾、都正常執行後再手動提交
  • 連接池:導入驅動、造properties設定檔填入連接要素、造DataSource、獲取連接
  • DbUtils:造QueryRunner用於CRUD,造ResultSetHandler處理查來的結果,用closeQuietly()關閉連接

上次修改於 2021-12-30