JDBC第二部分—statment和preparedStatement


statment和preparedStatement

Statement使用的注意事項

statement的作用是執行一段靜態的sql語句,並返回處理的結果集對象。但是statement存在一定的弊端:

①sql語句需要傳遞參數時,我們需要對sql語句進行拼接,這樣會很麻煩,影響我們的開發效率。

②使用statement執行sql語句時會存在sql注入問題,簡單來說就是利用沒有對用戶輸入的數據進行檢查,利用非法的sql語句完成惡意行為的做法

下面寫了一個簡單的登錄例子,用來測試statement存在的sql注入問題。

正常訪問數據庫時:

 @Test
    public void queryDataByStatement()  {
        /*
        簡單的登錄模塊測試statement的弊端
         */
        Scanner scanner = new Scanner(System.in);
        System.out.print("請輸入用戶賬號:");
        String userNum = scanner.nextLine();
        System.out.print("請輸入用戶密碼:");
        String password = scanner.nextLine();

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

        try {
            //1.獲取數據庫的連接:使用自定義工具類
            connection = MyJDBCUtils.getConnection();
            //2.創建一個statement實例
            statement = connection.createStatement();
            //3.創建sql語句:此處需要對sql語句進行拼串操作,略微麻煩
            String sql="select user,password from user_table where user='"+userNum+"' and password='"+password+"'";
            //4.執行sql語句
            resultSet = statement.executeQuery(sql);
            //5.對返回結果進行簡單處理
            if (resultSet.next())
                System.out.println("登錄成功!!!");
            else
                System.out.println("登錄失敗!!!");
            //6.關閉數據庫的連接,此時statement和結果集也需要被關閉:使用自定義工具類
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            MyJDBCUtils.closeConnection(connection,statement,resultSet);
        }

    }

 

返回的結果是正常的:

 


 

當惡意訪問數據庫時:

 
         
@Test
public void queryDataByStatement() {
/*
簡單的登錄模塊測試statement的弊端
*/
Scanner scanner = new Scanner(System.in);
System.out.print("請輸入用戶賬號:");
String userNum = scanner.nextLine();
System.out.print("請輸入用戶密碼:");
String password = scanner.nextLine();

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

try {
//1.獲取數據庫的連接:使用自定義工具類
connection = MyJDBCUtils.getConnection();
//2.創建一個statement實例
statement = connection.createStatement();
//3.創建sql語句:此處需要對sql語句進行拼串操作,略微麻煩

String sql="select user,password from user_table where user = '"+userNum+" 'and password = '"+password+"'";
//4.執行sql語句
resultSet = statement.executeQuery(sql);
//5.對返回結果進行簡單處理
if (resultSet.next())
System.out.println("登錄成功!!!");
else
System.out.println("登錄失敗!!!");
//6.關閉數據庫的連接,此時statement和結果集也需要被關閉:使用自定義工具類
} catch (Exception e) {
e.printStackTrace();
} finally {
MyJDBCUtils.closeConnection(connection,statement,resultSet);
}
 
        

執行結果為:此時賬號和密碼明顯不對,但是卻能登陸成功。

 之所以出現這種情況,是因為statement沒有對sql語句進行事先的編譯,我們傳入什么,它就會向數據庫發送什么數據,當賬號和密碼是上圖中的情況時,sql語句實際為下圖的情況,這個就叫做sql注入

 

 

 


PreparedStatement使用注意事項

為了解決statement中sql注入的問題,我們需要使用preparedStatement來替換原有的statement。

  preparedStatement是statement的一個子接口,它的好處是可以對sql語句進行預編譯,在創建preparedStatement實例時已經知道了自己要執行的sql語句是什么

使用preparedStatement完成對數據庫的增刪改查操作

1.使用prepareStatement向user表中添加一條數據

@Test
    public void addUserByPre()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫的連接
            connection = MyJDBCUtils.getConnection();
            //2.創建sql語句:此時數據庫中設計的主鍵id是自增的,我們可以不用主動添加
            //?此時代表占位符,表明你將要傳遞的參數,有幾個?代表需要傳遞幾個參數
            String sql="insert into `user`(name,password,address,phone) values(?,?,?,?)";
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.注入占位符(
            // 兩個參數,第一個為需要注入的占位符的下標,第二個參數為具體注入的內容.這里需要注意的是下標是從1開始的而不是0)。
            preparedStatement.setString(1,"王寶強");
            preparedStatement.setString(2,"123456");
            preparedStatement.setString(3,"河北省秦皇島市");
            preparedStatement.setString(4,"12345678910");
            //5.執行相關操作
             preparedStatement.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉相應連接
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }

    }

2. 使用preparedStatement修改user表中的某條數據

 @Test
    public void updateUserByPre()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫連接
            connection = MyJDBCUtils.getConnection();
            //2.創建sql語句
            String sql="update user set name = ? where id = ?";
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.填充占位符
            preparedStatement.setString(1,"許三多");
            preparedStatement.setInt(2,8);
            //5.執行操作
            int i = preparedStatement.executeUpdate();
            if (i != 0)
                System.out.println("修改成功");
            else
                System.out.println("修改失敗");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉資源
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }

    }

3.使用preparedStatement刪除user表中的一條數據

  @Test
    public void deleteUserByPre(){
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            try {
                //1.獲取數據庫連接
                connection = MyJDBCUtils.getConnection();
                //2.創建sql語句
                String sql="delete from user  where id = ?";
                //3.創建preparedStatement對象
                preparedStatement = connection.prepareStatement(sql);
                //4.填充占位符
                preparedStatement.setInt(1,6);
                //5.執行操作
                int i = preparedStatement.executeUpdate();
                if (i != 0)
                    System.out.println("刪除成功");
                else
                    System.out.println("刪除失敗");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //6.關閉資源
                MyJDBCUtils.closeConnection(connection,preparedStatement);
            }
    }

4.觀察代碼可以看出來,增刪改三種方法的代碼是具有一定的重復性的,唯一的區別無非就是sql語句和占位符的不同,因此我們可以考慮將三種方法封裝為同一個方法,調用的時候只需要傳遞sql語句和占位符即可。代碼如下(可自行測試,這里就不再寫測試代碼了)

 public static void updateDataBase(String sql,Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫的連接
            connection = getConnection();
            //2.創建sql語句,此步驟可直接使用傳遞進來的sql語句
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.填充占位符
        /*
            1.首先要獲取占位符的個數,因為可變形參的個數就是占位符的個數,所以只需要獲取args的長度即可
            2.填充占位符,使用for循環來做,需要注意的是下標的問題
         */
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //5.執行操作
            int i = preparedStatement.executeUpdate();
            if (i != 0)
                System.out.println("此次操作成功!");
            else
                System.out.println("此次操作失敗!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉資源
            closeConnection(connection,preparedStatement);
        }

    }

5.使用preparedStatement查詢user表中的一條記錄(查詢和增刪改是不同的,因為查詢需要有返回的結果集)

  public User queryUser(String sql,Object ...args) { Connection connection = null; PreparedStatement prepareStatement = null; ResultSet resultSet = null; try { //1.獲取數據庫連接 connection = MyJDBCUtils.getConnection(); //2.創建sql語句 //3.創建preparedStatement對象 prepareStatement = connection.prepareStatement(sql); //4.填充占位符 for (int i = 0; i < args.length; i++) { prepareStatement.setObject(i+1,args[i]); } //5.執行操作 resultSet = prepareStatement.executeQuery(); //6.將查詢出來的數據封裝成為一個對象 //1.獲取一個元數據對象 ResultSetMetaData metaData = resultSet.getMetaData(); //2.通過元數據對象來獲取該條數據中一共有多少列 int columnCount = metaData.getColumnCount(); if (resultSet.next()){ /* resultSet.next()有些類似與迭代器中的hashNext()和next()的結合體 在迭代器中,hasNext()的作用是判斷下一個位置是否為空,next()如果下一個位置不為空,指針下移並且返回當前對象,如果為空,則結束操作 而resultSet.next()的作用是判斷下一個位置是否為空,並且指針下移,返回的是Boolean值 */ //3.創建一個對象實體 User user = new User(); //如何將數據封裝進一個JavaBean中呢?此時並不知道取出的元素具體是什么類型的! //在resultSet中提供了一個方法用來獲取查詢到的元數據(元數據:修飾查詢出來數據的數據,可以參考元注解的概念), // 4.使用元數據來獲取當前這一條數據的每一列的列名和對應的列值 for (int i = 0; i < columnCount; i++) { String columnName = metaData.getColumnName(i + 1); Object columnValue = resultSet.getObject(i + 1); //5.使用反射技術動態的為bean對象中的屬性賦值 Field declaredField = User.class.getDeclaredField(columnName); declaredField.setAccessible(true); declaredField.set(user,columnValue); } //System.out.println(user); return user; } } catch (Exception e) { e.printStackTrace(); } finally { //6.關閉資源  MyJDBCUtils.closeConnection(connection,prepareStatement,resultSet); } return null; }

 這個時候需要提供一個對應的JavaBean實例

6.同樣的,我們也可以封裝一個函數用來獲取不同的表中的單條數據

   public static <T> T getBeanByPre(Class<T> clazz,String sql,Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //1.獲取到數據庫的連接
            connection = getConnection();
            //2.創建一個preparedStatement實例
            preparedStatement = connection.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //4.執行操作
            resultSet = preparedStatement.executeQuery();
            //5.獲取查詢記錄的元數據
            ResultSetMetaData metaData = resultSet.getMetaData();
            //6.獲取查詢記錄中的列數
            int columnCount = metaData.getColumnCount();
            if (resultSet.next()){
                //7.使用反射創建一個bean是咧
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //8.獲取到每一列的別名
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //9.獲取到每一列的值
                    Object columnValue = resultSet.getObject(i + 1);

                    //10.使用反射為bean中的屬性賦值
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t,columnValue);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //11.關閉資源
            closeConnection(connection,preparedStatement,resultSet);
        }
        return null;
    }

7.我們也可以封裝一個函數用來獲取不同的表中的多條數據

public static <T>List<T> getBeanListByPre(Class<T> clazz, String sql, Object ...args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //1.獲取到數據庫的連接
            connection = getConnection();
            //2.創建一個preparedStatement實例
            preparedStatement = connection.prepareStatement(sql);
            //3.填充占位符
            for (int i = 0; i < args.length; i++) {
                preparedStatement.setObject(i+1,args[i]);
            }
            //4.執行操作
            resultSet = preparedStatement.executeQuery();
            //5.獲取查詢記錄的元數據
            ResultSetMetaData metaData = resultSet.getMetaData();
            //6.獲取查詢記錄中的列數
            int columnCount = metaData.getColumnCount();
            //7.創建list集合
            ArrayList<T> list = new ArrayList<>();
            while (resultSet.next()){
                //7.使用反射創建一個bean實例
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    //8.獲取到每一列的別名
                    String columnLabel = metaData.getColumnLabel(i + 1);
                    //9.獲取到每一列的值
                    Object columnValue = resultSet.getObject(i + 1);

                    //10.使用反射為bean中的屬性賦值
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t,columnValue);
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //11.關閉資源
            closeConnection(connection,preparedStatement,resultSet);
        }
        return null;
    }

 之所以preparedStatement可以解決sql注入問題,是因為它的預編譯sql語句的功能,在我們生成preparedStatement對象時,已經使用過了sql語句。在占位符還沒有填充之前,它就已經對sql語句進行了解析。對於剛開始的登錄測試來說,創建preparedStatement對象時,sql語句表示的就是user = ?and password = ?,無論傳入什么值,它都會認為是user和password的值。以此避免了sql注入問題

 preparedStatement同時還可以操作Blob類型的數據,可以更高效的實現批量操作

 


 

preparedStatement和statement的對比

1.代碼的可讀性和可維護性:

在statement中,sql語句需要我們主動的去拼接,這樣在可讀性的體驗上是非常差的,同時維護起來也是十分的不方便。在preparedStatement中,對於參數的傳遞我們使用占位符來替代,這樣大大的提高了代碼的可讀性,同時維護起來也不容易出錯。

2.preparedStatement能最大可能地提高性能:

(1)DBServer會對預編譯的語句提供性能的優化。因為預編譯語句有可能會被重復的調用,所以語句在被DBServer的編譯器編譯之后的執行代碼會被緩存下來,那么下次調用時,只要是相同的預編譯語句就不需要編譯,只要將參數直接傳入編譯過的預編譯語句中就會得到執行。

(2)在statement語句中,即使是相同操作但是因為數據內容不一樣,所以整個語句本身不能匹配,不存在緩存語句的意義,這樣每執行一次都要對傳入的語句編譯執行一次。

3.preparestatement還可以有效的防止sql注入問題。

 


批量插入的對比

使用statement進行批量插入

 @Test
    public void statementInsert() {
        Connection connection = null;
        Statement statement = null;
        try {
            //1.獲取數據庫連接
            connection = MyJDBCUtils.getConnection();
            //2.創建statement對象
            statement = connection.createStatement();
            //3.模擬演示批量插入操作

            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 2000; i++) {
                //4.提供sql語句
                String sql = "insert into goods(name) values('name-"+i+"')";
                statement.executeUpdate(sql);
            }
            long endTime = System.currentTimeMillis();
            System.out.println("statement批量插入所需要的時間為:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            MyJDBCUtils.closeConnection(connection,statement);
        }

    }

所花費的時間為:

 

 

 

使用prepareStatement實現批量插入操作

 @Test
    public void preparedStatementInsert()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫連接
            connection = MyJDBCUtils.getConnection();
            //2.提供sql語句
            String sql = "insert into goods(name) values(?)";
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                preparedStatement.executeUpdate();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement執行批量插入所需要的時間為:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花費的時間為

 

 

 

使用batch相關操作提高批量插入的效率:mysql默認是關閉批處理的,我們需要在url中添加參數來開啟批處理的操作。?rewriteBatchedStatements=true

 @Test
    public void preparedStatementInsert1()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫連接
            connection = MyJDBCUtils.getConnection();
            //2.提供sql語句
            String sql = "insert into goods(name) values(?)";
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i <= 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                //6.在填充占位符之后不立刻提交,而是積累到一定數量之后一起提交
                    //1.攢sql
                preparedStatement.addBatch();

                if (i % 500 == 0)
                {
                    //2.提交積攢的500條sql語句
                    preparedStatement.executeBatch();

                    //3.清空batch
                    preparedStatement.clearBatch();
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement中使用batch執行批量插入所需要的時間為:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花費的時間為:

 

 

 

批量提交的方式四:默認情況下,數據庫中的DML操作每執行一次就會自動提交,也就是說在上述的第三種方式中,每500次都會重新執行一次insert,意味着每500次都會提交一次數據然后將數據寫入到數據庫,寫入操作也是會花費一定的時間的,如果數據量小可能看不出來,但是如果是百萬條數據的話,時間方面的差異就體現出出來了。對此,我們可以關閉數據庫的默認提交操作,等把數據全部讀入之后在進行統一的提交

  @Test
    public void preparedStatementInsert2()  {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            //1.獲取數據庫連接
            connection = MyJDBCUtils.getConnection();

            //關閉數據庫的自動提交功能
            connection.setAutoCommit(false);

            //2.提供sql語句
            String sql = "insert into goods(name) values(?)";
            //3.創建preparedStatement對象
            preparedStatement = connection.prepareStatement(sql);
            //4.批量插入
            long startTime = System.currentTimeMillis();
            for (int i = 0; i <= 2000; i++) {
                //5.填充占位符
                preparedStatement.setString(1,"name"+i);
                //6.在填充占位符之后不立刻提交,而是積累到一定數量之后一起提交
                //1.攢sql
                preparedStatement.addBatch();

                if (i % 500 == 0)
                {
                    //2.提交積攢的500條sql語句
                    preparedStatement.executeBatch();

                    //3.清空batch
                    preparedStatement.clearBatch();
                }
            }
            //對數據進行統一的提交
            connection.commit();
            
            long endTime = System.currentTimeMillis();
            System.out.println("preparedStatement中使用batch執行批量插入所需要的時間為:"+(endTime-startTime));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.關閉操作
            MyJDBCUtils.closeConnection(connection,preparedStatement);
        }
    }

所花費的時間為:

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM