1. 程式人生 > >Java 資料持久化系列之JDBC

Java 資料持久化系列之JDBC

前段時間小冰在工作中遇到了一系列關於資料持久化的問題,在排查問題時發現自己對 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