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); } }
所花費的時間為: