Java 數據持久化系列之JDBC

10{icon} {views}

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

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

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

廢話不多說,我們先來看 JDBC。

JDBC 定義

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

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

JDBC 基礎代碼示例

單純使用 JDBC 的代碼邏輯十分簡單,我們就以最為常用的MySQL 為例,展示一下使用 JDBC 來建立數據庫連接、執行查詢語句和遍歷結果的過程。

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 函數被調用。

參考

    • https://blog.csdn.net/wl044090432/article/details/60768342
    • https://blog.csdn.net/luanlouis/article/details/29850811

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益