天天看點

【JDBC系列】- 核心API之preparedstatement用法

作者:馬士兵老師

引言

這篇來學習一下preparedstatement這個API的用法。

Statement的缺點

Statement的最大缺點是會有SQL注入的風險。使用Statement時,SQL查詢通常是通過字元串拼接建構的。如果沒有正确地驗證和處理使用者輸入,可能會導緻SQL注入攻擊,即惡意使用者可以在輸入中插入惡意SQL代碼,進而執行非授權的資料庫操作。

例如以下SQL語句

java複制代碼String username = "lyd";
String sql = "SELECT * FROM tb_user WHERE username = '" + username + "'";
           

假設username是通過輸入傳過來的,如果再沒做校驗的情況下,當輸入的值為lyd' or '1' = '1最後形成的SQL語句就變成了

java複制代碼String sql = "SELECT * FROM tb_user WHERE username = 'lyd' or '1' = '1'";
           

這樣我們就能查到指定資料之外的資料了,這就會造成安全隐患。

更多缺點如下

SQL注入風險: 使用Statement時,SQL查詢通常是通過字元串拼接建構的。如果沒有正确地驗證和處理使用者輸入,可能會導緻SQL注入攻擊,即惡意使用者可以在輸入中插入惡意SQL代碼,進而執行非授權的資料庫操作。 性能問題: Statement執行SQL查詢時,每次都要将SQL語句發送給資料庫伺服器進行編譯。對于重複執行的查詢,這會導緻性能問題,因為資料庫伺服器需要重複編譯相同的查詢語句。 可讀性和維護性: 使用Statement時,SQL查詢通常以字元串形式嵌入Java代碼中,這可能導緻SQL語句在代碼中分散分布,降低代碼的可讀性和維護性。特别是對于複雜的SQL查詢,代碼的可讀性将變得更差。 不支援參數化查詢: 在Statement中,SQL查詢通常是寫死在Java代碼中的,沒有很好地支援參數化查詢。參數化查詢是一種更安全和高效的方式,它允許将參數值與SQL語句分開,避免了SQL注入風險,并且可以利用資料庫的查詢緩存。

預編譯statement - PreparedStatement

使用preparedstatement與使用statement的流程是一緻的,隻是預編譯的需要多加一部去傳入參數。

1、簡單例子示範預編譯statement

用個簡單的例子來示範一下。與statement沒什麼太大的差別,主要是建立傳輸sql的對象不一樣,不然其他基本都是相同的,這裡需要注意的是,在使用預編譯statement的SQL語句需要用“?”占位符來替代動态資料。在建立PreparedStatement對象之後,需要根據下标進行給占位符指派,順序是從左往右,從1開始。最後執行與statement大緻相同,單純查詢就是用executeQuery。最後得到的結果集可以直接解析。

java複制代碼/**
 * @Author: lyd
 * @Description: 使用預編譯Statement
 * @Date: 2023/7/23
 */
public class JDBCPreparedStatement {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String name = "lyd";
        // 1、注冊驅動
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2、建立連接配接
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
        // 3、建立preparedstatement
        // 3.1、編寫sql語句 不包含動态部分,動态值用占位符?來替代
        String sql = "SELECT * FROM tb_user WHERE username = ?";
        // 3.2、建立preparedstatement,傳入動态值
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        // 3.3、占位符指派: 從左向右,從1開始。set是有類型的,可以使用Object
        preparedStatement.setObject(1, name);
        // 4、發送sql  查詢就用executeQuery
        ResultSet resultSet = preparedStatement.executeQuery();
        // 5、解析結果
        while (resultSet.next()) {
            System.out.println(resultSet.getString(1));
            System.out.println(resultSet.getString(2));
            System.out.println(resultSet.getString(3));
        }
        // 6、釋放資源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}
           

2、進一步了解PreparedStatement

通過上面的簡單執行個體,我們可以大緻了解預編譯statement無論是在安全上還是性能上都會比statement更勝一籌。我們進入PreparedStatement接口中可以看到他實際上是繼承了Statement。

【JDBC系列】- 核心API之preparedstatement用法

官方也提供了一個例子,通過下标給占位符去指派。這樣就能夠很好的解決SQL注入的風險。

1)、執行DML語句

使用預編譯statement執行DML語句,操作步驟與查詢是一緻的,隻是在發送SQL的時候使用的是executeUpdate,他傳回的并不是結果集,而是行數,正如我們用Navicat執行時顯示的幾行受影響一樣,成功執行了多少行資料就會傳回多少行,當執行失敗就會傳回0。

代碼如下

java複制代碼public class JDBCDMLPreparedStatement {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        String name = "lyd";
        // 1、注冊驅動
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2、建立連接配接
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
        // 3、建立preparedstatement
        // 3.1、編寫sql語句 不包含動态部分,動态值用占位符?來替代
        String sql = "INSERT INTO tb_user(id,username,address) VALUES (?,?,?)";
        // 3.2、建立preparedstatement,傳入動态值
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        // 3.3、占位符指派: 從左向右,從1開始。set是有類型的,可以使用Object
        preparedStatement.setObject(1, 3);
        preparedStatement.setObject(2, "opo");
        preparedStatement.setObject(3, "quanzhou");
        // 4、發送sql  DML語句使用
        int row = preparedStatement.executeUpdate();
        // 5、解析結果
        System.out.println(row + "行受影響");
        // 6、釋放資源
        preparedStatement.close();
        connection.close();
    }
}
           

執行後,可以在資料庫中看到新增了這條資料。

【JDBC系列】- 核心API之preparedstatement用法

2)、執行DQL語句

這次我們擷取所有資料,并且周遊結果集,将每行的資料儲存到一個連結清單當中,用map進行key-value進行映射。

注冊驅動

還是一樣的方法,我們采用反射的方式來注冊驅動。

java複制代碼Class.forName("com.mysql.cj.jdbc.Driver");
           

建立連接配接

Java複制代碼Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/cloud_user", "root", "12356");
           

建立preparedstatement

因為不需要傳入參數,也就沒有使用占位符,然而這裡也就不需要去對占位符進行指派,隻需要建立preparedstatement對象,将SQL語句放入就行。

java複制代碼// 編寫sql語句 不包含動态部分,動态值用占位符?來替代
String sql = "SELECT * FROM tb_user";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
           

發送SQL

java複制代碼// 發送sql  查詢就用executeQuery
ResultSet resultSet = preparedStatement.executeQuery();
           

解析結果集

我們需要将每行資料進行提取,并且提取列名作為key而列值作為value來進行類似映射封裝,最後存在List中。在之前就知道了ResultSet包含了一個行的遊标,通過next可以将遊标移動到下一行。然後可以通過get方法,來根據列标簽或者是下表來擷取值。如果要達成我們的目的,可以直接自定義key名稱,再通過get方法擷取值。雖然這樣可以做到,但是靈活度不高。而本次要學習的是靈活度相對高一點的方案。

我們看一下資料集,這有3行資料,每行有3列,我們可以通過next方法來做到移動遊标擷取行資料,那麼列呢?我們要如何才能獲得資料庫的列名或者查詢資料的列标簽呢?

【JDBC系列】- 核心API之preparedstatement用法

ResultSetMetaData

ResultSet中有一個getMetaData()方法,它能夠傳回一個ResultSetMetaData對象,這個對象就是目前結果集列資訊對象。我們通過源碼可以看到這個列資訊對象中包含了會話和字段等資訊。

java複制代碼public class ResultSetMetaData implements java.sql.ResultSetMetaData {
    private Session session;
    private Field[] fields;
    boolean useOldAliasBehavior = false;
    boolean treatYearAsDate = true;
    private ExceptionInterceptor exceptionInterceptor;
    // ...
}
           

我們可以通過getColumnCount()方法擷取這個結果集有多少個字段(列)。在package com.mysql.cj.jdbc.result.ResultSetMetaData#getColumnCount,實際上就是統計了 Field[] fields 這個字段數組的長度。

java複制代碼public int getColumnCount() throws SQLException {
    try {
        return this.fields.length;
    } catch (CJException var2) {
        throw SQLExceptionsMapping.translateException(var2, this.exceptionInterceptor);
    }
}
           

這樣我們就可以在每行資料集中去周遊每列資料,這裡需要注意的是,周遊的下标是從1開始到列長度數量。

擷取列的資訊就通過ResultSetMetaData對象擷取,擷取行相關資訊就通過ResultSet對象擷取,ResultSetMetaData能夠擷取列名或者列标簽,建議使用列标簽,因為有可能在查詢的時候取了别名。

java複制代碼ResultSet resultSet = preparedStatement.executeQuery();
List<Map> list = new ArrayList<>();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 解析結果
while (resultSet.next()) {
    Map map = new HashMap<>();
    // 周遊資料
    for (int i = 1; i <= columnCount; i++) {
        // 通過下标擷取列名 - 通過 ResultSetMetaData對象擷取
        String columnLabel = metaData.getColumnLabel(i);
        // 通過下标擷取列值 - 通過 ResultSet對象擷取
        String value = resultSet.getString(i);
        map.put(columnLabel, value);
    }
    list.add(map);
}
           

運作之後的結果

【JDBC系列】- 核心API之preparedstatement用法

ResultSetMetaData對象中還有許多的方法,如果是學習mybatis底層源碼的時候,這些都是必不可少的資料。通過斷點可以檢視這個對象中的屬性有哪些。這些後續會慢慢深入研究,并把學習成果分享出來。

【JDBC系列】- 核心API之preparedstatement用法
原文連結:https://juejin.cn/post/7258810540939149370

繼續閱讀