[瘋狂Java]JDBC:PreparedStatement預編譯執行SQL語句


1. SQL語句的執行過程——Statement直接執行的弊病:

    1) SQL語句和編程語言一樣,僅僅就會普通的文本字符串,首先數據庫引擎無法識別這種文本字符串,而底層的CPU更不理解這些文本字符串(只懂二進制機器指令),因此SQL語句在執行之前肯定需要編譯的;

    2) SQL語句的執行過程:提交SQL語句 -> 數據庫引擎對SQL語句進行編譯得到數據庫可執行的代碼 -> 執行SQL代碼;

    3) 現在再來看Statement的執行機制:

         i. Statement的execute系列方法直接將SQL語句作為參數傳入並提交給數據庫執行;

         ii. 也就是說每提交一次都需要先經過編譯然后再執行;

         iii. 那么有一個最大的問題就是如果一條SQL語句需要再短時間內被反復執行,那么每次都需要經過編譯這樣不是效率非常非常低嗎??

!!可能你會問哪有需要反復大量執行的相同語句呢?仔細一想可能是的,因此上面說的並不完全精確,精確地講應該是反復執行一系列模型相似的語句,比如:

 

[sql]  view plain  copy
 
  1. insert into table1 values(1, "Peter");  

!你每次執行時只是values中的值不同,但是總體的語句還是insert into語句,那么你每次提交都需要編譯豈不是會把大把時間浪費在編譯上面了,非常不值;

 

 

2. PreparedStatement的預編譯機制——類似於Properties配置文件:

    1) 通過Connection(conn)還可以得到另一種SQL語句對象,即PreparedStatement,該方法就是:PreparedStatement Connection.prepareStatement(String sql);

    2) 注意細節:這里就不是create了,而是准備一個SQL語句句柄,精確地講是一個PreparedStatement語句句柄,並且創建該句柄時直接傳入了SQL語句;

    3) 預編譯機制:

         i. 調用prepareStatement時會直接將該SQL語句提交給數據庫進行編譯,得到的PreparedStatement句柄其實是一個預編譯好的SQL語句;

         ii. 之后調用PreparedStatement的execute方法(其execute系列方法都是無參的),就直接將該預編譯的語句提交給數據庫直接運行而不需要再編譯一次了;

         iii. 因此這種方法只需要編譯一次就夠了,后面就是直接提交執行無需再編譯,因此效率最高;

    4) 而預編譯語句最大的特點就是支持占位符(支持的占位符就是?,代表任意長度的字符串),比如:insert into table1 values(null, ?, ?);

!!也就是說可以用帶占位符的SQL語句來創建預編譯SQL句柄:PreparedStatement pstmt = conn.prepareStatement("insert into table1 values(null, ?, ?)");

!!這樣的語句也能通過,也可以成功編譯,並且可以再后期決定這些占位符具體的值,即使改變這些值后依然不需要編譯而直接提交運行;

    5) 設定占位符具體的值:

        i. 可以使用PreparedStatement的setXxx方法設定預編譯語句中占位符的值;

        ii. 其原型是這個模式的:void PreparedStatement setXxx(int parameterIndex, Xxx x);

        iii. Xxx幾乎涵蓋了所有Java基礎類型(String、int、double、Date等等);

        iv. parameterIndex代表語句中第幾個占位符(從1開始),而x就是具體設定的值;

    6) 設置好占位符的值之后無需編譯可以直接提交執行;

!!這種機制其實是跟Properties配置文件完全一樣,修改值后無需編譯即可運行!

    7) 直接提交執行:

         i. 使用PreparedStatement的execute系列方法即可,和Statement的execute系列方法相對應,只不過無需SQL語句參數了,因為已經存在預編譯的SQL語句了,因此都是無參的,就表示直接提交執行;

         ii. 方法:

             a. ResultSet PreparedStatement.executeQuery();

             b. int PreparedStatement.executeUpdate();

             c. boolean PreparedStatement.execute();

!!返回值的意義和Statement的完全相同;

3. 比較直接提交和預編譯運行的執行效率(各執行100次):

 

[java]  view plain  copy
 
  1. public class Test {  
  2.   
  3.     private String driver;  
  4.     private String url;  
  5.     private String user;  
  6.     private String pass;  
  7.       
  8.     private void insertUseStatement() throws Exception {  
  9.         long start = System.currentTimeMillis();  
  10.         try ( // 過了try塊會直接釋放連接資源  
  11.             Connection conn = DriverManager.getConnection(url, user, pass);  
  12.             Statement stmt = conn.createStatement()  
  13.         ) {  
  14.             for (int i = 0; i < 100; i++) {  
  15.                 stmt.executeUpdate("insert into student_table values(" + "null, '姓名" + i + "', 1)");  
  16.             }  
  17.             System.out.println("使用Statment耗時:" + (System.currentTimeMillis() - start));  
  18.         }  
  19.     }  
  20.       
  21.     private void insertUsePreparedStatement() throws Exception {  
  22.         long start = System.currentTimeMillis();  
  23.         try ( // 因此需要在另一個方法中重新連接  
  24.             Connection conn = DriverManager.getConnection(url, user, pass);  
  25.             PreparedStatement pstmt = conn.prepareStatement("insert into student_table values(null, ?, 1)")  
  26.         ) {  
  27.             for (int i = 0; i < 100; i++) {  
  28.                 pstmt.setString(1,  "姓名" + i);  
  29.                 pstmt.executeUpdate();  
  30.             }  
  31.             System.out.println("使用PreparedStatement耗時:" + (System.currentTimeMillis() - start));  
  32.         }  
  33.           
  34.     }  
  35.     public void init() throws Exception {  
  36.         Properties props = new Properties();  
  37.         props.load(new FileInputStream("mysql.ini"));  
  38.         driver = props.getProperty("driver");  
  39.         url = props.getProperty("url");  
  40.         user = props.getProperty("user");  
  41.         pass = props.getProperty("pass");  
  42.           
  43.         insertUseStatement();  
  44.         insertUsePreparedStatement();         
  45.     }  
  46.   
  47.     public static void main(String[] args) throws Exception {  
  48.         new Test().init();  
  49.     }  
  50.   
  51. }  

!!可以看到預編譯比直接提交少用很多時間;

 

 

4. 預編譯SQL的安全性能:

    1) 首先最明顯的一點就是Statement不支持占位符,因此SQL語句中包含可變內容時必須要進行字符串拼接,而字符串拼接不僅加大了編程的難度,降低了代碼的可讀性,而且非常容易發生因拼接錯誤而導致地極難發現的bug,因此從這點來看PreparedStatement更加安全;

    2) 其次是字符串拼接容易埋下SQL注入的漏洞:

         i. SQL注入是指黑客在應用程序端惡意地往查詢信息中填寫SQL語句實現入侵(因為客戶端輸入的要查詢的信息往往都是一些正常信息,例如姓名、電話、學號等,沒人會無聊地往里面輸入代碼之類的東西);

         ii. 一個典型的例子:比如SQL語句的目的是select * from member_table where name = input_name and pass = input_pass; input_name和input_pass是用戶在客戶端輸入框中輸入的賬號名和登陸密碼,如果該查詢語句能查詢到該用戶(即返回記錄不為空)就表示該用戶登陸成功;

如果用預編譯占位符來表示該語句就是:select * from member_table where name = ? and pass = ?;   // 然后后期用input_name和input_pass來填補占位符,這沒什么問題

但如果用Statement拼接的方式來寫該語句就是:"select * from member_table where name = '" + input_name + "' and pass = '" + input_pass +"'";,而此時如果黑客在任意一個輸入框(賬戶名或者密碼)中填入'or true or'(就比如賬戶名輸入框吧),那么得到的結果就是:

select * from member_table where name = '' or true or '' and pass = '';

!!也就是說最后的邏輯表達式變成了name = ''、true、'' and pass = ''三者通過or連接在了一起,因為or了一個true因此整個where表達式的結果都是true,因此必然會select處記錄,因此即使這樣也可以正常登陸!!這就被成功入侵了

         iii. 這最主要是由於不帶占位符的拼接必須要用單引號'來包裹SQL字符串,而占位符的填寫無需單引號,JDBC會自動將Java變量轉換成純字符串然后再自動加上SQL單引號填入占位符中,即使填入的變量是String str = "'Lala'",那么JDBC也會將其中的單引號' '轉化成純字符單引號處理,而不會被當做SQL的特殊字符單引號'來處理,因為在SQL中單引號'是字符串常量符號!

 

5. 占位符使用問題注意:

    1) 占位符只能占位SQL語句中的普通值,決不能占位表名、列名、SQL關鍵字(select、insert等);

    2) 原因很簡單,以為PreparedStatement的SQL語句是要預編譯的,如果關鍵字、列名、表名等被占位那就直接代表該SQL語句語法錯誤而無法編譯,會直接拋出異常,因此只有不影響編譯的部分可用占位符占位!!


免責聲明!

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



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