本文轉自:http://www.cnblogs.com/meronzhang/archive/2012/09/28/2707374.html
作者:Billy Newport
本文講述了如何正確的使用prepared statements。為什么它可以讓你的應用程序運行的更快,和同樣的讓數據庫操作變的更快。
為什么Prepared Statements非常重要?如何正確的使用它?
數據庫有着非常艱苦的工作。它們接受來自眾多並發的客戶端所發出的SQL查詢,並盡可能快的執行查詢並返回結果。處理statements是一個開銷昂貴的操作,不過現在有了Prepared Statements這樣的方法,可以將這種開銷降到最低。可是這種優化需要開發者來完成。所以本文會為大家展示如何正確的使用Prepared Statements才能使數據庫操作達到最優化。
數據庫是如何執行一個statement的?
顯然,我不會在這里寫出很多的細節,我們只關注最關鍵的部分。當一個數據庫收到一個statement后,數據庫引擎會先解析statement,然后檢查其是否有語法錯誤。一旦statement被正確的解析,數據庫會選出執行statement的最優途徑。遺憾的是這個計算開銷非常昂貴。數據庫會首先檢查是否有相關的索引可以對此提供幫助,不管是否會將一個表中的全部行都讀出來。數據庫對數據進行統計,然后選出最優途徑。當決創建查詢方案后,數據庫引擎會將它執行。
存取方案(Access Plan)的生成會占用相當多的CPU。理想的情況是,當我們多次發送一個statement到數據庫,數據庫應該對statement的存取方案進行重用。如果方案曾經被生成過的話,這將減少CPU的使用率。
Statement Caches
數據庫已經具有了類似的功能。它們通常會用如下方法對statement進行緩存。使用statement本身作為key並將存取方案存入與statement對應的緩存中。這樣數據庫引擎就可以對曾經執行過的statements中的存取方案進行重用。舉個例子,如果我們發送一條包含SELECT a, b FROM t WHERE c = 2的statement到數據庫,然后首先會將存取方案進行緩存。當我們再次發送相同的statement時,數據庫會對先前使用過的存取方案進行重用,這樣就降低了CPU的開銷。
注意,這里使用了整個statement為key。也就是說,如果我們發送一個包含SELECT a, b FROM t WHERE c = 3的statement的話,緩存中會沒有與之對應的存取方案。這是因為“c=3”與曾經被緩存過的“c=2”不同。所以,舉個例子:
for (int i = 0; i < 1000; i++) {
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = " + i);
ResultSet rs = Ps.executeQuery();
rs.close();
ps.close();
}
在這里緩存不會被使用,因為每一次迭代都會發送一條包含不同SQL語句的statement給數據庫。並且每一次迭代都會生成一個新的存取方案。現在讓我們來看看下一段代碼:
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = ?");
for (int i = 0; i < 1000; i++) {
ps.setInt(1, i);
ResultSet rs = ps.executeQuery();
rs.close();
ps.close();
}
這樣就具有了更好的效率,這個statement發送給數據庫的是一條帶有參數“?”的SQL語句。這樣每次迭代會發送相同的statement到數據庫,只是參數“c=?”不同。這種方法允許數據庫重用statement的存取方案,這樣就具有了更好的效率。這可以讓你的應用程序速度更快,並且使用更少的CPU,這樣數據庫服務器就可以為更多的人提供服務。
PreparedStatement與J2EE服務器
當我們使用J2EE服務器時事情會變的比較復雜。通常,一個perpared statement會同一個單獨的數據庫連接相關聯。當數據庫連接被關閉時prepared statement也會被丟棄。通常,一個胖客戶端會獲取一個數據庫連接並將其一直保持到退出。它會用“餓漢”(eagerly)或“懶漢”(lazily)方式創建所有的parepared statements。“餓漢”方式會在應用啟動時創建一切。“懶漢”方式意味着只有在使用的時候才去創建。“餓漢”方式會使應用程序在啟動的時候梢有延遲,但一旦啟動后就會運行的相當理想。“懶漢”方式使應用程序啟動速度非常快(但不會做任何准備工作),當需要使用prepared statement的時候再去創建。這樣,在創建全部statement的過程中,性能是非常不穩定的,但一旦創建了所有statement后,它會像“餓漢”式應用程序一樣具有很好的運行效果。請根據你的需要來選擇最好的方式,是快速啟動?還是前后一致的性能。
J2EE應用的問題是它不會像這樣工作,連接只會在請求期間被保持。那意味着必須每一次請求的時候都創建prepared statement。這遠沒有胖客戶端那種一直保持prepared statement的執行性能好。J2EE廠商已經注意到了這個問題,並且提供了連接池(ConnectionPool)以避免這種問題。
當J2EE服務器提供了一個連接給你的應用程序時,其實它並沒有給你真正的數據庫連接,你只是獲得了一個包裝器(Wrapper)。你可以去看看你所獲得的連接的類名以證實這一點。它並不是一個JDBC連接,而是一個由應用服務器創建的類。所有的JDBC操作都會被應用服務器的連接池管理器所代理。所有的JDBC ResultSets,statements,CallableStatements,preparedStatements等都會被包裝並以一個“代理對象”(Proxy Object)的形式返回給應用程序。當你關閉了連接,這些對象會被標記為失效,並被垃圾回收器所回收。
通常,如果你對一個數據庫連接執行close,那這個連接會被JDBC驅動程序關閉。但我們需要在J2EE服務器執行close的時候數據庫連接會被返回連接池。我們可以創建一個像真正的連接一樣的JDBC Connection代理類來解決這個問題。它有一個對真正連接的引用。當我們執行一個連接上的方法時,代理會將操作轉給真正的連接。但是,當我們對一個連接執行close時,這個連接並不會關閉,而是會送回連接池,並可以被其他請求所使用。一個已被准備過的prepared statement也會因此而得到重用。
J2EE PreparedStatement Cache
J2EE服務器的連接池管理器已經實現了緩存的使用。J2EE服務器保持着連接池中每一個連接准備過的prepared statement列表。當我們在一個連接上調用preparedStatement時,應用服務器會檢查這個statement是否曾經准備過。如果是,這個PreparedStatement會被返回給應用程序。如果否,調用會被轉給JDBC驅動程序,然后將新生成的statement對象存入連接緩存。
每個連接都有一個緩存的原因是因為:JDBC驅動程序就是這樣工作的。任何prepared statement都是由指定的連接所返回的。
如果我們想利用這個緩存的優勢,那就如前面所說的,使用參數化的查詢語句可以在緩存中找到曾經使用過的statement。大部分應用服務器允許你調整prepared statements緩存的大小。
摘要
我們絕對應該使用包含參數化的查詢語句的prepared statement。這樣數據庫就會重用准備過的存取方案。緩存適用於整個數據庫,所以,如果你安排所有的應用程序使用相同的參數化SQL語句,然后你的其他應用程序就可以重用被准備過的prepared statement。這是應用服務器的一個優勢,因為所有的數據庫操作都集中在數據庫操作層(Database Access Layer,包括O/R映射,實體Bean,JDBC等)。
第二,正確的使用prepared statement也是利用prepared statement的緩存優勢的關鍵。由於應用程序可以重用准備過的prepared statement,也就減少了調用JDBC驅動程序的次數,從而提高了應用程序的性能。這樣就擁有了可以與胖客戶端比肩的效率,卻又不需要總維持一個連接。
使用參數化的prepared statement,你的應用程序會具有更好的性能。
///////////////
1.PreparedStatement是預編譯的,對於批量處理可以大大提高效率. 也叫JDBC存儲過程
2.使用 Statement 對象。在對數據庫只執行一次性存取的時侯,用 Statement 對象進行處理。PreparedStatement 對象的開銷比Statement大,對於一次性操作並不會帶來額外的好處。
3.statement每次執行sql語句,相關數據庫都要執行sql語句的編譯,preparedstatement是預編譯得,preparedstatement支持批處理
4.
Code Fragment 1:
String updateString = "UPDATE COFFEES SET SALES = 75 " + "WHERE COF_NAME LIKE ′Colombian′";
stmt.executeUpdate(updateString);
Code Fragment 2:
PreparedStatement updateSales = con.prepareStatement("UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ? ");
updateSales.setInt(1, 75);
updateSales.setString(2, "Colombian");
updateSales.executeUpdate();
片斷2和片斷1的區別在於,后者使用了PreparedStatement對象,而前者是普通的Statement對象。 PreparedStatement對象不僅包含了SQL語句,而且大多數情況下這個語句已經被預編譯過,因而當其執行時,只需DBMS運行SQL語句, 而不必先編譯。當你需要執行Statement對象多次的時候,PreparedStatement對象將會大大降低運行時間,當然也加快了訪問數據庫的 速度。
這種轉換也給你帶來很大的便利,不必重復SQL語句的句法,而只需更改其中變量的值,便可重新執行SQL語句。選擇PreparedStatement對 象與否,在於相同句法的SQL語句是否執行了多次,而且兩次之間的差別僅僅是變量的不同。如果僅僅執行了一次的話,它應該和普通的對象毫無差異,體現不出 它預編譯的優越性。
5.執行許多SQL語句的JDBC程序產生大量的Statement和PreparedStatement對象。通常認為 PreparedStatement對象比Statement對象更有效,特別是如果帶有不同參數的同一SQL語句被多次執行的時候。 PreparedStatement對象允許數據庫預編譯SQL語句,這樣在隨后的運行中可以節省時間並增加代碼的可讀性。
然而,在Oracle環境中,開發人員實際上有更大的靈活性。當使用Statement或PreparedStatement對象時,Oracle數據庫 會緩存(和JDBC緩存是兩回事)SQL語句以便以后使用。在一些情況下,由於驅動器自身需要額外的處理和在Java應用程序和Oracle服務器間增加的網絡活動,執行 PreparedStatement對象實際上會花更長的時間(因此,使用statement的最好時機是頻繁的參數--是參數項目而不是參數的值--變化時的SQL語句)。
然而,除了緩沖的問題之外,至少還有一個更好的原因使我們在企業應用程序中更喜歡使用PreparedStatement對象,那就是安全性。傳遞給 PreparedStatement對象的參數可以被強制進行類型轉換,使開發人員可以確保在插入或查詢數據時與底層的數據庫格式匹配。
當處理公共Web站點上的用戶傳來的數據的時候,安全性的問題就變得極為重要。傳遞給PreparedStatement的字符串參數會自動被驅動器忽 略。最簡單的情況下,這就意味着當你的程序試着將字符串“D'Angelo”插入到VARCHAR2中時,該語句將不會識別第一個“,”,從而導致悲慘的 失敗。幾乎很少有必要創建你自己的字符串忽略代碼。
在Web環境中,有惡意的用戶會利用那些設計不完善的、不能正確處理字符串的應用程序。特別是在公共Web站點上,在沒有首先通過 PreparedStatement對象處理的情況下,所有的用戶輸入都不應該傳遞給SQL語句。此外,在用戶有機會修改SQL語句的地方,如HTML的 隱藏區域或一個查詢字符串上,SQL語句都不應該被顯示出來。
在執行SQL命令時,我們有二種選擇:可以使用PreparedStatement對象,也可以使用Statement對象。無論多少次地使用同一個 SQL命令,PreparedStatement都只對它解析和編譯一次。當使用Statement對象時,每次執行一個SQL命令時,都會對它進行解析 和編譯。
第一:
prepareStatement會先初始化SQL,先把這個SQL提交到數據庫中進行預處理,多次使用可提高效率。
createStatement不會初始化,沒有預處理,沒次都是從0開始執行SQL
第二:
prepareStatement可以替換變量
在SQL語句中可以包含?,可以用ps=conn.prepareStatement("select * from Cust where ID=?");
int sid=1001;
ps.setInt(1, sid);
rs = ps.executeQuery();
可以把?替換成變量。
而Statement只能用 int sid=1001;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from Cust where ID="+sid);
來實現。
第三:
prepareStatement會先初始化SQL,先把這個SQL提交到數據庫中進行預處理,多次使用可提高效率。
createStatement不會初始化,沒有預處理,沒次都是從0開始執行SQL