JDBC基礎整理(快速入門、自定義鏈接池)

JDBC是什麼

Java數據庫鏈接(Java DataBase Connectivity)

是Java提供的一套用於鏈接、操做數據庫的一套接口規範,即便用Java語言來操做數據庫java

爲何須要有JDBC

就像車子跑起來須要發動機的驅動,顯卡等電腦硬件想運行起來須要顯卡驅動,使用Java語言操做相應的數據庫也須要相應的驅動mysql

但存在一個問題,數據庫的種類很是繁多,會致使相應的數據庫驅動也很繁多,不一樣數據庫的API又會存在很大的差異
爲了操做不一樣數據庫而去學習不一樣數據庫的API,對於開發人員而言,無疑增長了不少沒必要要的學習成本sql

好在Java官方提供了JDBC接口規範,數據庫廠商須要實現該接口來編寫各自的數據庫驅動
這樣對於開發人員而言只須要熟悉JDBC API就能操做各種廠商的數據庫了,即面向接口編程數據庫

交互的結構

圖片來自於百度編程

JDBC的組成

JDBC主要由java.sql以及javax.sql兩個包組成
能夠先簡單瞭解如下對象,更多詳細的描述能夠查看JDK API文檔
java.sql.DriverManager 驅動的管理類,用於數據庫驅動的註冊
java.sql.Connection 數據庫鏈接接口,處理客戶端與數據庫之間的全部交互
java.sql.Statement 語句接口,封裝了要向數據庫操做的SQL語句信息
java.sql.ResultSet 結果集接口,封裝了查詢語句返回的結果信息服務器

JDBC快速入門

以操做MySQL數據庫爲例,先來看一段最簡單的JDBC操做數據庫的代碼,而後再根據代碼瞭解相應的步驟session

// 註冊驅動
Class.forName("com.mysql.cj.jdbc.Driver");
// 創建鏈接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?characterEncoding=UTF-8", "root", "123456");
// 建立語句
Statement statement = connection.createStatement();
// 執行查詢
ResultSet resultSet = statement.executeQuery("select * from user");
// 遍歷結果
while(resultSet.next()) {
    String host = resultSet.getString("Host");
    String user = resultSet.getString("User");
    System.out.println(host + ":" + user);
}
// 釋放資源
resultSet.close();
statement.close();
connection.close();

控制檯打印的查詢結果併發

localhost:root
localhost:mysql.session
localhost:mysql.sys
%:root

JDBC操做數據庫的步驟

1. 註冊驅動

Maven引入驅動依賴

上述代碼既然是以MySQL數據庫爲例,天然須要在項目中引入MySQL驅動的Maven構件,當前最新版本爲8.0.12
爲了方便寫測試代碼,引入Junit的Maven構件,當前最新版本爲4.12oracle

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>LATEST</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>LATEST</version>
</dependency>

com.mysql.jdbc.Driver與com.mysql.cj.jdbc.Driver之間的區別

爲了操做數據庫,須要先獲取到實現了java.sql.Driver接口的驅動
MySQL驅動包提供了com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver兩個數據庫驅動類ide

com.mysql.jdbc.Driver是5.x版本的驅動中的實現類,已通過時
com.mysql.cj.jdbc.Driver是6.x及以上版本的驅動中的實現類

com.mysql.jdbc.Driver代碼的靜態代碼塊中描述了該驅動實現類已通過時

static {
    System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
            + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}

DriverManager方法註冊驅動

有了數據庫廠商提供的驅動後,要先在程序中註冊加載驅動
java.sql.DrvierManager提供了註冊驅動的方法

public static void registerDriver(java.sql.Driver driver)
public static void registerDriver(java.sql.Driver driver, DriverAction da)

註冊驅動須要傳入java.sql.Driver接口的實現,傳入com.mysql.jdbc.Driver驅動進行註冊

DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver())

Class.forName反射註冊驅動

但經過對com.mysql.cj.jdbc.Driver代碼的查看,發現類中靜態代碼塊已經對當前驅動進行了註冊,會形成二次註冊
因此開發人員不須要手動去註冊MySQL驅動,只要讓JVM加載com.mysql.cj.jdbc.Driver或其子類便可完成驅動的註冊

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

那最經常使用的方式是經過反射直接加載目標Class

Class.forName("com.mysql.cj.jdbc.Driver")

且這種方式不一樣於傳入對象的硬編碼,字符串的方式在更換數據庫的時候也更方便

2. 創建鏈接

加載並註冊完驅動後就能夠創建起鏈接來實現對數據庫的訪問了
java.sql.DriverManager提供了三種方法獲取Connection對象,客戶端與數據庫的全部交互都經過該對象完成

public static Connection getConnection(String url, java.util.Properties info)
public static Connection getConnection(String url, String user, String password)
public static Connection getConnection(String url)

最經常使用的方式是經過URL以及數據庫帳號密碼來獲取鏈接,如今鏈接本地MySQL下名爲mysql的數據庫並設置編碼屬性爲UTF-8

DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?characterEncoding=UTF-8", "root", "123456")

URL鏈接的組成與寫法

MySQL寫法:jdbc:mysql://localhost:3306/databaseName
MySQL本地默認端口號簡寫:jdbc:mysql:///databaseName
Oracle寫法:jdbc:oracle:thin:@localhost:1521:databaseName

主要由jdbc協議、相應數據庫的子協議以及主機名、端口號和數據庫名稱構成
其中thin爲oracle數據庫的一種驅動方式,對應的還有oci方式

3. 操做數據

鏈接到本地的MySQL數據庫後開始對數據庫進行操做
操做數據庫要經過SQL語句來操做,首先要經過connection對象建立語句對象來實現對數據庫的SQL操做

Statement statement = connection.createStatement();

Statement接口提供了execute、executeQuery、executeUpate、addBatch、executeBatch等方法來執行SQL

  • execute:執行任意的SQL語句
  • executeQuery:執行select語句
  • executeUpdate:執行insert、update、delete或SQL DDL等語句
  • addBatch:添加多條SQL語句到批處理中
  • clearBatch:清除批量SQL語句
  • executeBatch:批量執行SQL語句

查詢的結果封裝在ResultSet對象中,即存放告終果對象的一個容器
ResultSet對象提供了next方法來移動容器中的指針,相似於集合中迭代器的hasNext方法
經過循環判斷就能夠遍歷拿到每一個結果對象的值

ResultSet resultSet = statement.executeQuery("select * from user");
while(resultSet.next()) {
    String host = resultSet.getString("Host");
    String user = resultSet.getString("User");
    System.out.println(host + ":" + user);
}

數據庫字段類型與JDBC方法對應表

數據庫字段有不一樣的類型,ResultSet對象能夠經過不一樣的方法獲取不一樣類型的數據

MySQL字段類型 ResultSet對應方法 方法返回類型
BIT(1) getBoolean(String) boolean
BIT getBytes(String) byte[]
TINYINT getByte(String) byte
SMALLINT getShort(String) short
INT getInt(String) int
BIGINT getLong(String) long
CHAR、 VARCHAR getString(String) java.lang.String
TEXT、BLOB getClob(String) getBlob(String) java.sql.Clob java.sql.Blob
DATE getDate(String) java.sql.Date
TIME getTime(String) java.sql.Time
TIMESTAMP getTimestamp(String) java.sql.Timestamp

ResultSet還提供了其餘不少方式來獲取字段的值
例如getObject(int index)、getObject(String columnName)分別根據字段位置和字段名稱來獲取任意類型的數據
更多相關的方法能夠查看JDK API找到相應的類瞭解

4. 釋放資源

操做完數據庫後須要釋放資源,依次斷開對數據庫的操做和鏈接

傳統close方法手動釋放資源

傳統的close方式必需要在finally中編寫確保資源必定會被釋放
但代碼相對而言比較重複繁瑣

@Test
public void closeResource() {
    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;
    try {
        // 省略部分代碼
        // ......
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    } finally {
        // 釋放資源
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            resultSet = null;
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            statement = null;
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            connection = null;
        }
    }
}

AutoCloseable自動釋放資源

JDK7開始提供了AutoCloseable接口,該接口的主要功能是幫助開發人員自動釋放資源
ResultSet、Statement、Connection接口都繼承了AutoCloseable接口
使用AutoCloseable接口管理資源須要使用JDK7的try-catch-resources語法
建立的資源在退出`try-block代碼塊時會自動調用該資源的close方法,釋放的順序爲先建立後釋放

@Test
public void autoCloseable() {
    // 省略部分代碼
    // ......
    try {
        Class.forName(driverClass);
        // try-catch-resources語法的try-block代碼塊
        try (Connection connection = DriverManager.getConnection(url, username, password);
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql)) {
            while(resultSet.next()) {
                String host = resultSet.getString("Host");
                String user = resultSet.getString("User");
                System.out.println(host + ":" + user);
            }
        }
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
}

爲何要釋放資源

數據庫是部署在服務器上的,服務器有着相應的硬件配置
程序對數據庫的操做與鏈接都會佔用服務器的CPU、內存等硬件資源
當程序處於空閒狀態,對數據庫沒有任何操做時,應及時釋放資源好讓數據庫能分配其餘程序資源
資源的過多佔用可能會致使服務器宕機中止工做而致使嚴重的後果
說的直白一點就是不要上完了廁所還要一直霸佔着坑位

JDBC工具類封裝

經過上述代碼的流程,能夠發現每次使用JDBC操做數據庫都要先註冊驅動、創建鏈接而後再操做數據庫、最後釋放資源
其中註冊驅動、創建鏈接以及釋放資源都是重複的,能夠封裝一個工具類來消除這種重複的編碼操做
使開發人員只須要關注SQL的編寫以及對結果的處理

public class JDBCUtil {

    private static final String DRIVERCLASS;
    private static final String URL;
    private static final String USERNAME;
    private static final String PASSWORD;

    static{
        Properties pro = new Properties();
        // 經過ClassLoader類加載器從classpath路徑下加載配置了數據庫信息的屬性文件
        InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
        try {
            pro.load(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        DRIVERCLASS = pro.getProperty("driverClass");
        URL = pro.getProperty("url");
        USERNAME = pro.getProperty("username");
        PASSWORD = pro.getProperty("password");
    }
    
    // 註冊驅動
    private static void loadDriver(){
        try {
            Class.forName(DRIVERCLASS);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    // 獲取鏈接
    public static Connection getConnection(){
        // 加載驅動
        loadDriver();
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL, USERNAME , PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}

在代碼中直接編寫數據庫配置信息的硬編碼方式顯然是不利於維護與修改的
因此將配置信息編寫在classpath路徑下的db.properties文件中
再經過類加載器讀取文件獲取文件中的鍵值對,完成驅動的註冊與鏈接的創建,資源的釋放則經過try-catch-resources實現

@Test
public void jdbcUtil() {
    String sql = "select * from user";
    try (Connection connection = JDBCUtil.getConnection();
         Statement statement = connection.createStatement();
         ResultSet resultSet = statement.executeQuery(sql)) {
        while(resultSet.next()) {
            String host = resultSet.getString("Host");
            String user = resultSet.getString("User");
            System.out.println(host + ":" + user);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

結合封裝的JDBC工具類以前的代碼能獲得這樣進一步的精簡

爲何須要數據庫鏈接池

JDBCUtil存在的問題:
每次訪問數據庫都要從新創建鏈接,而創建鏈接是很是耗時的,當程序頻繁訪問數據庫會形成程序性能的降低

解決的方案:
在一個容器中初始化必定數量的數據庫鏈接,程序每次訪問數據庫直接從容器中獲取到鏈接,執行完對數據庫的操做後再把鏈接歸還到容器中,這個容器便稱之爲數據庫鏈接池

自定義一個最簡單的鏈接池

Java官方提供了數據庫鏈接池的接口規範,實現javax.sql.DataSource接口來編寫鏈接池

public class DataSourceUtil implements DataSource {

    /**
     * 存放鏈接的容器
     */
    private List<Connection> connectionPool = new ArrayList<>();
    
    public DataSourceUtil() {
        addConnection();
    }

    /**
     * 初始化鏈接池
     */
    private void addConnection() {
        // 初始化10個鏈接
        for (int i = 0; i < 10; i++) {
            connectionPool.add(JDBCUtil.getConnection());
        }
    }

    /**
     * 從鏈接池中獲取鏈接
     * @return 鏈接對象
     */
    @Override
    public Connection getConnection() {
        // 若是鏈接池空了,對鏈接池進行擴容
        if (connectionPool.isEmpty()) {
            addConnection();
        }
        return connectionPool.remove(0);
    }

    /**
     * 歸還鏈接到鏈接池中
     * @param connection 鏈接對象
     */
    public void closeConnection(Connection connection) {
        connectionPool.add(connection);
    }
    
    // ......省略部分代碼
}

使用該鏈接池進行數據庫操做測試

@Test
public void dataSourceUtil() {
    String sql = "select * from user";
    DataSourceUtil dataSourceUtil = new DataSourceUtil();
    Connection connection = dataSourceUtil.getConnection();
    try (Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql)) {
        while(resultSet.next()) {
            String host = resultSet.getString("Host");
            String user = resultSet.getString("User");
            System.out.println(host + ":" + user);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 使用DataSourceUtil手動歸還鏈接
        dataSourceUtil.closeConnection(connection);
    }
}

本身寫的數據源畢竟是比較簡單的,並不會涉及到方方面面的問題
好在有各類強大的開源的鏈接池能夠供平時開發時使用,例如hikari、dbcp、c3p0等經常使用數據源

關於滾動結果集

使用JDBC查詢數據庫會返回ResultSet對象,默認經過Statement createStatement()方法建立執行返回的結果集是隻能向下滾動且只讀的
使用Statement createStatement(int resultSetType, int resultSetConcurrency)來指定結果集的類型以及策略

經常使用結果集類型
resultSetType
    TYPE_FORWARD_ONLY          結果集只能向下
    TYPE_SCROLL_INSENSITIVE    能夠滾動,不能修改記錄
    TYPE_SCROLL_SENSITIVE      能夠滾動,能夠修改記錄
    
經常使用結果集併發策略
resultSetConcurrency
    CONCUR_READ_ONLY           只讀,不能修改
    CONCUR_UPDATABLE           結果集能夠修改

另外ResultSet還提供了不少的方法來對結果集內的指針進行操做

next()                         移動到下一行
previous()                     移動到前一行
absolute(int row)              移動到指定行
beforeFirst()                  移動到resultSet最前面
afterLast()                    移動到resultSet最後面
updateRow()                    更新行數據

編寫測試例子查詢結果集中第四行的數據

@Test
public void resultSet() {
    String sql = "select * from user";
    try (Connection connection = JDBCUtil.getConnection();
         Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
         ResultSet resultSet = statement.executeQuery(sql)) {
        resultSet.absolute(4);
        String host = resultSet.getString("Host");
        String user = resultSet.getString("User");
        System.out.println(host + ":" + user);
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

執行結果

%:root

關於SQL注入

使用Statement對象執行SQL語句是直接採用拼接字符串的方式,會致使SQL注入的危害
例如登陸校驗用戶是否存在的簡單SQL注入,SQL以下

String sql = "select * from user where username = ' + username + ' and password = ' + password + ' ";

若是用戶傳入的username 爲xxx ' or ' 1 = 1,SQL將會變成

String sql = "select * from user where username = 'xxx' or '1 = 1' and password = ''";

這樣致使表達式username = 'xxx' or '1 = 1'結果老是爲true,因此無論密碼填什麼都無所謂了
執行這樣的SQL將致使用戶並無輸入正確的帳號密碼卻經過了驗證進入到了系統

使用PreparedStatement防止SQL注入

Statement會使數據庫頻繁編譯SQL,可能形成數據庫緩衝區溢出
PreparedStatement對象支持SQL預編譯,還能經過佔位符來管理變量從而防止SQL注入
此時username再傳入xxx ' or ' 1 = 1,程序會將其當作總體,在數據庫查找username爲"xxx ' or ' 1 = 1"的用戶

String sql = "select * from user where username = ? and password = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 設置第一個、第二個佔位符的值
preparedStatement.setString(1, user.getUsername());
preparedStatement.setString(2, user.getPassword());
相關文章
相關標籤/搜索