JDBC 中preparedStatement和Statement區別
一、概念
PreparedStatement是用來執行SQL查詢語句的API之一,Java提供了 Statement、PreparedStatement 和 CallableStatement三種方式來執行查詢語句,其中 Statement 用於通用查詢, PreparedStatement 用於執行引數化查詢,而 CallableStatement則是用於儲存過程。同時PreparedStatement還經常會在Java面試被提及,譬如:Statement與PreparedStatement的區別以及如何避免SQL注入式攻擊?這篇教程中我們會討論為什麼要用PreparedStatement?使用PreparedStatement有什麼樣的優勢?PreparedStatement又是如何避免SQL注入攻擊的?
1.PreparedStatement:
PreparedStatement是java.sql包下面的一個介面,用來執行SQL語句查詢,通過呼叫connection.preparedStatement(sql)方法可以獲得PreparedStatment物件。資料庫系統會對sql語句進行預編譯處理(如果JDBC驅動支援的話),預處理語句將被預先編譯好,這條預編譯的sql查詢語句能在將來的查詢中重用,這樣一來,它比Statement物件生成的查詢速度更快。
2.Statement
使用 Statement 物件。在對資料庫只執行一次性存取的時侯,用 Statement 物件進行處理。PreparedStatement 物件的開銷比Statement大,對於一次性操作並不會帶來額外的好處。
二、深入理解statement 和prepareStatement
1、使用Statement而不是PreparedStatement物件
JDBC驅動的最佳化是基於使用的是什麼功能. 選擇PreparedStatement還是Statement取決於你要怎麼使用它們. 對於只執行一次的SQL語句選擇Statement是最好的. 相反, 如果SQL語句被多次執行選用PreparedStatement是最好的.
PreparedStatement的第一次執行消耗是很高的. 它的效能體現在後面的重複執行. 例如, 假設我使用Employee ID, 使用prepared的方式來執行一個針對Employee表的查詢. JDBC驅動會發送一個網路請求到資料解析和優化這個查詢. 而執行時會產生另一個網路請求.在JDBC驅動中,減少網路通訊是最終的目的. 如果我的程式在執行期間只需要一次請求, 那麼就使用Statement. 對於Statement, 同一個查詢只會產生一次網路到資料庫的通訊.
對於使用PreparedStatement池的情況下, 本指導原則有點複雜. 當使用PreparedStatement池時, 如果一個查詢很特殊, 並且不太會再次執行到, 那麼可以使用Statement. 如果一個查詢很少會被執行,但連線池中的Statement池可能被再次執行, 那麼請使用PreparedStatement. 在不是Statement池的同樣情況下, 請使用Statement.
2、使用PreparedStatement的Batch功能
Update大量的資料時, 先Prepare一個INSERT語句再多次的執行, 會導致很多次的網路連線. 要減少JDBC的呼叫次數改善效能, 你可以使用PreparedStatement的AddBatch()方法一次性發送多個查詢給資料庫. 例如, 讓我們來比較一下下面的例子.
***為了區分 “Statement、PreparedStatement、PreparedStatement + 批處理” 這三者之間的效率,下面的示例執行過程都是在資料庫表t1中插入1萬條記錄,並記錄出所需的時間(此時間與電腦硬體有關)。實驗結果如下:
1.使用Statement物件 用時31秒
2.預編譯PreparedStatement 用時14秒
3.使用PreparedStatement + 批處理 用時485毫秒***
-------------------------------------------------------
1.使用Statement物件
使用範圍:當執行相似SQL(結構相同,具體值不同)語句的次數比較少
優點:語法簡單
缺點:採用硬編碼效率低,安全性較差。
原理:硬編碼,每次執行時相似SQL都會進行編譯
示例執行過程:
public void exec(Connection conn){
try {
Long beginTime = System.currentTimeMillis();
conn.setAutoCommit(false);//設定手動提交
Statement st = conn.createStatement();
for(int i=0;i<10000;i++){
String sql="insert into t1(id) values ("+i+")";
st.executeUpdate(sql);
}
Long endTime = System.currentTimeMillis();
System.out.println("Statement用時:"+(endTime-beginTime)/1000+"秒");//計算時間
st.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
執行時間:Statement用時:31秒
----------------------------------------------------------------
2.預編譯PreparedStatement
使用範圍:當執行相似sql語句的次數比較多(例如使用者登陸,對錶頻繁操作..)語句一樣,只是具體的值不一樣,被稱為動態SQL
優點:語句只編譯一次,減少編譯次數。提高了安全性(阻止了SQL注入)
缺點: 執行非相似SQL語句時,速度較慢。
原理:相似SQL只編譯一次,減少編譯次數
事例執行過程:
public void exec2(Connection conn){
try {
Long beginTime = System.currentTimeMillis();
conn.setAutoCommit(false);//手動提交
PreparedStatement pst = conn.prepareStatement("insert into t1(id) values (?)");
for(int i=0;i<10000;i++){
pst.setInt(1, i);
pst.execute();
}
conn.commit();
Long endTime = System.currentTimeMillis();
System.out.println("Pst用時:"+(endTime-beginTime)+"秒");//計算時間
pst.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
執行時間:Pst用時:14秒
------------------------------------------------------------------
3.使用PreparedStatement + 批處理
使用範圍:一次需要更新資料庫表多條記錄
優點:減少和SQL引擎互動的次數,再次提高效率,相似語句只編譯一次,減少編譯次數。提高了安全性(阻止了SQL注入)
缺點:
原理:批處理: 減少和SQL引擎互動的次數,一次傳遞給SQL引擎多條SQL。
名詞解釋:
PL/SQL引擎:在oracle中執行pl/sql程式碼的引擎,在執行中發現標準的sql會交給sql引擎進行處理。
SQL引擎:執行標準sql的引擎。
事例執行過程:
public void exec3(Connection conn){
try {
conn.setAutoCommit(false);
Long beginTime = System.currentTimeMillis();
PreparedStatement pst = conn.prepareStatement("insert into t1(id) values (?)");
for(int i=1;i<=10000;i++){
pst.setInt(1, i);
pst.addBatch();//加入批處理,進行打包
if(i%1000==0){//可以設定不同的大小;如50,100,500,1000等等
pst.executeBatch();
conn.commit();
pst.clearBatch();
}//end of if
}//end of for
pst.executeBatch();
Long endTime = System.currentTimeMillis();
System.out.println("pst+batch用時:"+(endTime-beginTime)+"毫秒");
pst.close();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
執行時間:pst+batch用時:485毫秒
三、區別
1.程式碼的可讀性和可維護性.
雖然用PreparedStatement來代替Statement會使程式碼多出幾行,但這樣的程式碼無論從可讀性還是可維護性上來說.都比直接用Statement的程式碼高很多檔次:
stmt.executeUpdate(“insert into tb_name (col1,col2,col2,col4) values (‘”+var1+”’,’”+var2+”’,”+var3+”,’”+var4+”’)”);//stmt是Statement物件例項
perstmt = con.prepareStatement(“insert into tb_name (col1,col2,col2,col4) values (?,?,?,?)”);
perstmt.setString(1,var1);
perstmt.setString(2,var2);
perstmt.setString(3,var3);
perstmt.setString(4,var4);
perstmt.executeUpdate(); //prestmt是 PreparedStatement 物件例項
不用我多說,對於第一種方法.別說其他人去讀你的程式碼,就是你自己過一段時間再去讀,都會覺得傷心.
2.PreparedStatement盡最大可能提高效能.
語句在被DB的編譯器編譯後的執行程式碼被快取下來,那麼下次呼叫時只要是相同的預編譯語句就不需要編譯,只要將引數直接傳入編譯過的語句執行程式碼中(相當於一個涵數)就會得到執行.這並不是說只有一個Connection中多次執行的預編譯語句被快取,而是對於整個DB中,只要預編譯的語句語法和快取中匹配.那麼在任何時候就可以不需要再次編譯而可以直接執行.而statement的語句中,即使是相同一操作,而由於每次操作的資料不同所以使整個語句相匹配的機會極小,幾乎不太可能匹配.比如:
insert into tb_name (col1,col2) values (‘11’,’22’);
insert into tb_name (col1,col2) values (‘11’,’23’);
即使是相同操作但因為資料內容不一樣,所以整個個語句本身不能匹配,沒有快取語句的意義.事實是沒有資料庫會對普通語句編譯後的執行程式碼快取.
當然並不是所以預編譯語句都一定會被快取,資料庫本身會用一種策略,比如使用頻度等因素來決定什麼時候不再快取已有的預編譯結果.以儲存有更多的空間儲存新的預編譯語句.
2.最重要的一點是極大地提高了安全性.
即使到目前為止,仍有一些人連基本的惡義SQL語法都不知道.
String sql = “select * from tb_name where name= ‘”+varname+”’ and passwd=’”+varpasswd+”’”;
如果我們把[’ or ‘1’ = ‘1]作為varpasswd傳入進來.使用者名稱隨意,看看會成為什麼?
select * from tb_name = ‘隨意’ and passwd = ” or ‘1’ = ‘1’;
因為’1’=’1’肯定成立,所以可以任何通過驗證.更有甚者:
把[‘;drop table tb_name;]作為varpasswd傳入進來,則:
select * from tb_name = ‘隨意’ and passwd = ”;drop table tb_name;有些資料庫是不會讓你成功的,但也有很多資料庫就可以使這些語句得到執行.
而如果你使用預編譯語句.你傳入的任何內容就不會和原來的語句發生任何匹配的關係.只要全使用預編譯語句,你就用不著對傳入的資料做任何過慮.而如果使用普通的statement,有可能要對drop,;等做費盡心機的判斷和過慮.
四、總結
關於PreparedStatement介面,需要重點記住的是:
1. PreparedStatement可以寫引數化查詢,比Statement能獲得更好的效能。
2. 對於PreparedStatement來說,資料庫可以使用已經編譯過及定義好的執行計劃,這種預處理語句查詢比普通的查詢執行速度更快。
3. PreparedStatement可以阻止常見的SQL注入式攻擊。
4. PreparedStatement可以寫動態查詢語句
5. PreparedStatement與java.sql.Connection物件是關聯的,一旦你關閉了connection,PreparedStatement也沒法使用了。
6. “?” 叫做佔位符。
7. PreparedStatement查詢預設返回FORWARD_ONLY的ResultSet,你只能往一個方向移動結果集的遊標。當然你還可以設定為其他型別的值如:”CONCUR_READ_ONLY”。
8. 不支援預編譯SQL查詢的JDBC驅動,在呼叫connection.prepareStatement(sql)的時候,它不會把SQL查詢語句傳送給資料庫做預處理,而是等到執行查詢動作的時候(呼叫executeQuery()方法時)才把查詢語句傳送個數據庫,這種情況和使用Statement是一樣的。
9. 佔位符的索引位置從1開始而不是0,如果填入0會導致java.sql.SQLException invalid column index異常。所以如果PreparedStatement有兩個佔位符,那麼第一個引數的索引時1,第二個引數的索引是2.
以上就是為什麼要使用PreparedStatement的全部理由,不過你仍然可以使用Statement物件用來做做測試。但是在生產環境下你一定要考慮使用 PreparedStatement 。