Java 數據持久化系列之JDBC

前段時間小冰在工做中遇到了一系列關於數據持久化的問題,在排查問題時發現本身對 Java 後端的數據持久化框架的原理都不太瞭解,只有不斷試錯,所以走了不少彎路。因而下定決心,集中精力學習了持久化相關框架的原理和實現,總結出這個系列。java

上圖是我根據相關源碼和網上資料總結的有關 Java 數據持久化的架構圖(只表明本人想法,若有問題,歡迎留言指出)。最下層就是今天要講的 JDBC,上一層是數據庫鏈接池層,包括 HikariCP 和 Druid等;再上一層是分庫分表中間件,好比說 ShardingJDBC;再向上是對象關係映射層,也就是 ORM,包括 Mybatis 和 JPA;最上邊是 Spring 的事務管理。mysql

本系列的文章會依次講解圖中各個開源框架的基礎使用,而後描述其原理和代碼實現,最後會着重分析它們之間是如何相互集成和配合的。sql

廢話很少說,咱們先來看 JDBC。數據庫

JDBC 定義

JDBC是Java Database Connectivity的簡稱,它定義了一套訪問數據庫的規範和接口。但它自身不參與數據庫訪問的實現。所以對於目前存在的數據庫(譬如Mysql、Oracle)來講,要麼數據庫製造商自己提供這些規範與接口的實現,要麼社區提供這些實現。後端

image.png

如上圖所示,Java 程序只依賴於 JDBC API,經過 DriverManager 來獲取驅動,而且針對不一樣的數據庫可使用不一樣的驅動。這是典型的橋接的設計模式,把抽象 Abstraction 與行爲實現Implementation 分離開來,從而能夠保持各部分的獨立性以及應對他們的功能擴展。設計模式

JDBC 基礎代碼示例

單純使用 JDBC 的代碼邏輯十分簡單,咱們就以最爲經常使用的MySQL 爲例,展現一下使用 JDBC 來創建數據庫鏈接、執行查詢語句和遍歷結果的過程。bash

public static void connectionTest(){

    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;

    try {
        // 1. 加載並註冊 MySQL 的驅動
        Class.forName("com.mysql.cj.jdbc.Driver").newInstance();

        // 2. 根據特定的數據庫鏈接URL,返回與此URL的所匹配的數據庫驅動對象
        Driver driver = DriverManager.getDriver(URL);
        // 3. 傳入參數,好比說用戶名和密碼
        Properties props = new Properties();
        props.put("user", USER_NAME);
        props.put("password", PASSWORD);

        // 4. 使用數據庫驅動建立數據庫鏈接 Connection
        connection = driver.connect(URL, props);

        // 5. 從數據庫鏈接 connection 中得到 Statement 對象
        statement = connection.createStatement();
        // 6. 執行 sql 語句,返回結果
        resultSet = statement.executeQuery("select * from activity");
        // 7. 處理結果,取出數據
        while(resultSet.next())
        {
            System.out.println(resultSet.getString(2));
        }

        .....
    }finally{
        // 8.關閉連接,釋放資源  按照JDBC的規範,使用完成後管理連接,
        // 釋放資源,釋放順序應該是: ResultSet ->Statement ->Connection
        resultSet.close();
        statement.close();
        connection.close();
    }
}

複製代碼

代碼中有詳細的註釋描述每一步的過程,相信你們也都對這段代碼十分熟悉。網絡

惟一要提醒的是使用完以後的資源釋放順序。按照 JDBC 規範,應該依次釋放 ResultSet,Statement 和 Connection。固然這只是規範,不少開源框架都沒有嚴格的執行,可是 HikariCP卻嚴格準守了,它能夠帶來不少優點,這些會在以後的文章中講解。架構

上圖是 JDBC 中核心的 5 個類或者接口的關係,它們分別是 DriverManager、Driver、Connection、Statement 和 ResultSet。框架

DriverManager 負責管理數據庫驅動程序,根據 URL 獲取與之匹配的 Driver 具體實現。Driver 則負責處理與具體數據庫的通訊細節,根據 URL 建立數據庫鏈接 Connection。

Connection 表示與數據庫的一個鏈接會話,能夠和數據庫進行數據交互。Statement 是須要執行的 SQL 語句或者存儲過程語句對應的實體,能夠執行對應的 SQL 語句。ResultSet 則是 Statement 執行後得到的結果集對象,可使用迭代器從中遍歷數據。

不一樣數據庫的驅動都會實現各自的 Driver、Connection、Statement 和 ResultSet。而更爲重要的是,衆多數據庫鏈接池和分庫分表框架也都是實現了本身的 Connection、Statement 和 ResultSet,好比說 HikariCP、Druid 和 ShardingJDBC。咱們接下來會常常看到它們的身影。

接下來,咱們依次看一下這幾個類及其涉及的操做的原理和源碼實現。

載入 Driver 實現

能夠直接使用 Class#forName的方式來載入驅動實現,或者在 JDBC 4.0 後則基於 SPI 機制來導入驅動實現,經過在 META-INF/services/java.sql.Driver 文件中指定實現類的方式來導入驅動實現,下面咱們就來看一下兩種方式的實現原理。

Class#forName 做用是要求 JVM 查找並加載指定的類,若是在類中有靜態初始化器的話,JVM 會執行該類的靜態代碼段。加載具體 Driver 實現時,就會執行 Driver 中的靜態代碼段,將該 Driver 實現註冊到 DriverManager 中。咱們來看一下 MySQL 對應 Driver 的具體代碼。它就是直接調用了 DriverManager的 registerDriver 方法將本身註冊到其維護的驅動列表中。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        // 直接調用 DriverManager的 registerDriver 將本身註冊到其中
        DriverManager.registerDriver(new Driver());
    }
}
複製代碼

SPI 機制使用 ServiceLoader 類來提供服務發現機制,動態地爲某個接口尋找服務實現。當服務的提供者提供了服務接口的一種實現以後,必須根據 SPI 約定在 META-INF/services 目錄下建立一個以服務接口命名的文件,在該文件中寫入實現該服務接口的具體實現類。當服務調用 ServiceLoader 的 load 方法的時候,ServiceLoader 可以經過約定的目錄找到指定的文件,並裝載實例化,完成服務的發現。

DriverManager 中的 loadInitialDrivers 方法會使用 ServiceLoader 的 load 方法加載目前項目路徑下的全部 Driver 實現。

public class DriverManager {
    // 程序中已經註冊的Driver具體實現信息列表。registerDriver類就是將Driver加入到這個列表
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    // 使用ServiceLoader 加載具體的jdbc driver實現
    static {
        loadInitialDrivers();
    }
    private static void loadInitialDrivers() {
        // 省略了異常處理
        // 得到系統屬性 jdbc.drivers 配置的值
        String drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 經過 ServiceLoader 獲取到Driver的具體實現類,而後加載這些類,會調用其靜態代碼塊
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
                return null;
            }
        });

        String[] driversList = drivers.split(":");
        // for 循環加載系統屬性中的Driver類。
        for (String aDriver : driversList) {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        }
    }
}
複製代碼

好比說,項目引用了 MySQL 的 jar包 mysql-connector-java,在這個 jar 包的 META-INF/services 文件夾下有一個叫 java.sql.Driver 的文件,文件的內容爲 com.mysql.cj.jdbc.Driver。而 ServiceLoader 的 load 方法找到這個文件夾下的文件,讀取文件的內容,而後加載出文件內容所指定的 Driver 實現。而正如以前所分析的,這個 Driver 類被加載時,會調用 DriverManager 的 registerDriver 方法,從而完成了驅動的加載。

Connection、Statement 和 ResultSet

當程序加載完具體驅動實現後,接下來就是創建與數據庫的鏈接,執行 SQL 語句而且處理返回結果了,其過程以下圖所示。

創建 Connection

建立 Connection 鏈接對象,可使用 Driver 的 connect 方法,也可使用 DriverManager 提供的 getConnection 方法,此方法經過 url 自動匹配對應的驅動 Driver 實例,而後仍是調用對應的 connect 方法返回 Connection 對象實例。

創建 Connection 會涉及到與數據庫進行網絡請求等大量費時的操做,爲了提高性能,每每都會引入數據庫鏈接池,也就是說複用 Connection,免去每次都建立 Connection 所消耗的時間和系統資源。

Connection 默認狀況下,對於建立的 Statement 執行的 SQL 語句都是自動提交事務的,即在 Statement 語句執行完後,自動執行 commit 操做,將事務提交,結果影響到物理數據庫。爲了知足更好地事務控制需求,咱們也能夠手動地控制事務,手動地在Statement 的 SQL 語句執行後進行 commit 或者rollback。

connection = driver.connect(URL, props);
// 將自動提交關閉
connection.setAutoCommit(false);

statement = connection.createStatement();
statement.execute("INSERT INTO activity (activity_id, activity_name, product_id, start_time, end_time, total, status, sec_speed, buy_limit, buy_rate) VALUES (1, '香蕉大甩賣', 1, 530871061, 530872061, 20, 0, 1, 1, 0.20);");
// 執行後手動 commit
statement.getConnection().commit();
複製代碼

Statement

Statement 的功能在於根據傳入的 SQL 語句,將傳入 SQL 通過整理組合成數據庫可以識別的執行語句(對於靜態的 SQL 語句,不須要整理組合;而對於預編譯SQL 語句和批量語句,則須要整理),而後傳遞 SQL 請求,以後會獲得返回的結果。對於查詢 SQL,結果會以 ResultSet 的形式返回。

當你建立了一個 Statement 對象以後,你能夠用它的三個執行方法的任一方法來執行 SQL 語句。

  • boolean execute(String SQL) : 若是 ResultSet 對象能夠被檢索,則返回的布爾值爲 true ,不然返回 false 。當你須要使用真正的動態 SQL 時,可使用這個方法來執行 SQL DDL 語句。
  • int executeUpdate(String SQL) : 返回執行 SQL 語句影響的行的數目。使用該方法來執行 SQL 語句,是但願獲得一些受影響的行的數目,例如,INSERT,UPDATE 或 DELETE 語句。
  • ResultSet executeQuery(String SQL) : 返回一個 ResultSet 對象。當你但願獲得一個結果集時使用該方法,就像你使用一個 SELECT 語句。

對於不一樣類型的 SQL 語句,Statement 有不一樣的接口與其對應。

接口 介紹
Statement 適合運行靜態 SQL 語句,不接受動態參數
PreparedStatement 計劃屢次使用而且預先編譯的 SQL 語句,接口須要傳入額外的參數
CallableStatement 用於訪問數據庫存儲過程

Statement 主要用於執行靜態SQL語句,即內容固定不變的SQL語句。Statement每執行一次都要對傳入的SQL語句編譯一次,效率較差。而 PreparedStatement則解決了這個問題,它會對 SQL 進行預編譯,提升了執行效率。

PreparedStatement pstmt = null;
    try {
        String SQL = "Update activity SET activity_name = ? WHERE activity_id = ?";
        pstmt = connection.prepareStatement(SQL);
        pstmt.setString(1, "測試");
        pstmt.setInt(2, 1);
        pstmt.executeUpdate();
    }
    catch (SQLException e) {
    }
    finally {
        pstmt.close();
    }
}
複製代碼

除此以外, PreparedStatement 還能夠預防 SQL 注入,由於 PreparedStatement 不容許在插入參數時改變 SQL 語句的邏輯結構。

PreparedStatement 傳入任何數據不會和原 SQL 語句發生匹配關係,無需對輸入的數據作過濾。若是用戶將」or 1 = 1」傳入賦值給佔位符,下述SQL 語句將沒法執行:select * from t where username = ? and password = ?。

ResultSet

當 Statement 查詢 SQL 執行後,會獲得 ResultSet 對象,ResultSet 對象是 SQL語句查詢的結果集合。ResultSet 對從數據庫返回的結果進行了封裝,使用迭代器的模式能夠逐條取出結果集中的記錄。

while(resultSet.next()) {
    System.out.println(resultSet.getString(2));
}
複製代碼

ResultSet 通常也建議使用完畢直接 close 掉,可是須要注意的是關閉 ResultSet 對象不關閉其持有的 Blob、Clob 或 NClob 對象。 Blob、Clob 或 NClob 對象在它們被建立的的事務期間會一直持有效,除非其 free 函數被調用。

我的博客,歡迎來玩

參考

相關文章
相關標籤/搜索