為什么 PreparedStatement 很重要, 以及怎樣"正確"使用他們.
數據庫有一個(gè)艱苦的工作. 它們不斷地從許多客戶(hù)端讀取 SQL 查詢(xún), 對數據進(jìn)行盡
可能高效的 查詢(xún). 處理語(yǔ)句可能成為一個(gè)代價(jià)較高的操作, 但是現在數據庫都是很
好的設計, 這樣這個(gè)困難 被減到最小. 但是這些優(yōu)化需要應用程序開(kāi)發(fā)者的協(xié)助,
這篇文章給你展示一下怎樣正確使用 PreparedStatement 來(lái)漂亮地幫助數據庫執行
這些優(yōu)化.
一個(gè)數據庫怎樣執行一條語(yǔ)句?
顯然, 不要希望這里有許多細節; 我們只看一下對這篇文章比較重要的部分. 當一個(gè)
數據庫接收 到一條語(yǔ)句的時(shí)候, 數據庫引擎首先解析這條語(yǔ)句, 查看語(yǔ)法錯誤. 一
旦語(yǔ)句解析了, 數據庫 需要找出最有效的方法來(lái)執行這條語(yǔ)句. 這個(gè)計算起來(lái)代價(jià)
很大. 數據庫檢查什么索引(如果有 的話(huà))能有所幫助, 或者它是否能全部讀出一張
表中所有的記錄. 數據庫根據這些關(guān)于數據庫所 存數據的統計數字來(lái)找出最好的
辦法. 一旦制訂出查詢(xún)方案, 就可以由數據庫引擎來(lái)執行.
需要 CPU 來(lái)產(chǎn)生訪(fǎng)問(wèn)方案. 理想的情況, 如果我們把相同的語(yǔ)句給數據庫發(fā)送兩
次, 我們期望 數據庫重用第一條記錄的訪(fǎng)問(wèn)方案. 這會(huì )比第二次重新產(chǎn)生方案要使
用較少的 CPU.
語(yǔ)句緩沖
數據庫可以進(jìn)行調節來(lái)做語(yǔ)句緩沖. 通常包含一些類(lèi)型的語(yǔ)句緩沖. 緩沖使用語(yǔ)句
本身作為關(guān)鍵 字, 訪(fǎng)問(wèn)方案和相應的語(yǔ)句存儲在緩沖區中. 這樣就允許數據庫引擎
對以前執行過(guò)的語(yǔ)句所使用 的訪(fǎng)問(wèn)方案進(jìn)行重用. 舉個(gè)例子來(lái)說(shuō), 如果我們向數據
庫發(fā)送這樣一條語(yǔ)句 "select a, b from t where c = 2", 計算好的訪(fǎng)問(wèn)方案就放入緩沖
區了. 如果我們以后再使用同樣的語(yǔ) 句, 數據庫就能重用以前的訪(fǎng)問(wèn)方案, 這樣就
能節省 CPU.
但是要注意, 整條語(yǔ)句是一個(gè)關(guān)鍵字. 例如, 如果我們后來(lái)發(fā)送的語(yǔ)句是 "select a,b
from t where c = 3", 那么就不會(huì )找出以前的訪(fǎng)問(wèn)方案. 因為 "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();
}
這里不會(huì )用到緩沖. 每次循環(huán)向數據庫發(fā)送一條不同的 SQL 語(yǔ)句. 每次循環(huán)都重新
計算新的訪(fǎng)問(wèn) 方案, 用這種方法我們會(huì )浪費大量的 CPU 周期. 但是, 看看下一個(gè)片
段:
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();
這樣就會(huì )高效得多. 發(fā)送給數據庫的語(yǔ)句在 sql 中使用 ´?´ 符號來(lái)參數化. 這意味著(zhù)
每次循環(huán) 發(fā)送的是同一條語(yǔ)句, 在 "c=?" 部分帶有不同的參數. 這樣就允許數據庫
重用語(yǔ)句的訪(fǎng)問(wèn)方案, 是程序在數據庫內部運行得更高效. 這基本上能使你的程序
運行得更快, 或者使數據庫用戶(hù)能更多 地使用 CPU.
PreparedStatement 和 J2EE 服務(wù)器
當我們使用 J2EE 服務(wù)器的時(shí)候, 事情會(huì )變得更加復雜. 通常情況下, 一個(gè)預先準備
好的語(yǔ)句 (prepared statement) 是和一個(gè)單獨的數據庫連接相關(guān)聯(lián)的. 當連接關(guān)閉時(shí),
語(yǔ)句就被丟棄 了. 一般來(lái)說(shuō), 一個(gè)胖客戶(hù)端應用程序在得到一個(gè)數據庫連接后會(huì )一
直保持到程序結束. 它會(huì )使用 兩種方法創(chuàng )建所有的語(yǔ)句: 急切創(chuàng )建(eagerly) 或者 懶
惰創(chuàng )建(lazily). Eagerly是說(shuō), 當程序啟動(dòng)時(shí)全部創(chuàng )建. Lazily是說(shuō)隨用隨創(chuàng )建. 急切
的方法會(huì )在程序啟動(dòng)時(shí)有些延時(shí), 但是一旦程序啟動(dòng)以后, 運行很好. 懶惰的方法啟
動(dòng)很快, 但是當程序運行時(shí), 預先準備的語(yǔ)句在第一次使用是創(chuàng )建. 這就會(huì )造成性能
不平衡, 知道所有的 語(yǔ)句都準備好了, 但是最終程序會(huì )和急切方法一樣快. 哪一種
最好要看你需要的是快速啟動(dòng)還是 均衡的性能.
一個(gè) J2EE 應用程序所帶來(lái)的問(wèn)題就是它不能像這樣工作. 它只在一個(gè)請求的生存
時(shí)間中保持一個(gè) 連接. 這意味著(zhù)在他處理每一個(gè)請求時(shí)都會(huì )重新創(chuàng )建語(yǔ)句, 就不象
胖客戶(hù)端只創(chuàng )建一次, 而不是每 個(gè)請求都創(chuàng )建那樣有效,
當 J2EE 服務(wù)器給你的程序一個(gè)連接時(shí), 并不是一個(gè)真正的連接, 而是一個(gè)經(jīng)過(guò)包裝
的. 你可以 通過(guò)查看那個(gè)連接的類(lèi)的名字來(lái)檢驗一下. 它不是一個(gè)數據庫的 JDBC
連接, 是你的服務(wù)器創(chuàng )建 的一個(gè)類(lèi). 通常, 如果你調用一個(gè)連接的 close 方法, 那么
jdbc 驅動(dòng)程序會(huì )關(guān)閉這個(gè)連接. 我們希望的是當 J2EE 應用程序調用 close 的時(shí)候,
連接會(huì )返回到連接池中. 我們通過(guò)設計一個(gè) 代理的 jdbc 連接類(lèi)來(lái)做這些, 但看起來(lái)
就象是實(shí)際的連接. 當我們調用這個(gè)連接的任何方法時(shí), 代理類(lèi)就會(huì )把請求前遞給
實(shí)際的連接. 但是, 當我們調用類(lèi)似 close 的方法時(shí), 并不調用實(shí)際 連接的 close 方
法, 只是簡(jiǎn)單地把連接返回給連接池, 然后把代理連接標記為無(wú)效, 這樣當它 被應
用程序重新使用時(shí), 我們會(huì )得到異常.
包裝是非常有用的, 因為它幫助 J2EE 應用程序服務(wù)器實(shí)現者比較聰明地加上預先
準備語(yǔ)句的 支持. 當程序調用 Connection.prepareStatement 時(shí), 由驅動(dòng)程序返回一
個(gè) PreparedStatement 對象. 當應用程序得到它時(shí), 保存這個(gè)句柄, 并且在請求完成
時(shí), 關(guān)閉 請求之前關(guān)閉這個(gè)句柄. 但是, 在連接返回到連接池之后, 以后被同樣或者
另一個(gè)應用程序重用時(shí), 那么, 我們就理論上希望同樣的 PreparedStatement 返回給
應用程序.
J2EE PreparedStatement 緩沖
J2EE PreparedStatement 緩沖由 J2EE 服務(wù)器內部的連接池管理器使用一個(gè)緩沖區
來(lái) 實(shí)現. J2EE 服務(wù)器在連接池中保存一個(gè)所有數據庫的預先準備語(yǔ)句的一個(gè)列表.
當一個(gè)程序 調用一個(gè)連接的 prepareStatement 方法時(shí), 服務(wù)器先檢查這個(gè)語(yǔ)句是否
已經(jīng)有了, 如果 是, 相應的 PreparedStatement 就在緩沖區內, 就返回給應用程序, 如
果不是, 請求就 會(huì )傳遞給 jdbc 驅動(dòng)程序, 請求/預先準備語(yǔ)句 對象就會(huì )加入到緩沖
區里.
對于每一個(gè)連接我們需要一個(gè)緩沖區, 因為這是 jdbc 驅動(dòng)程序的工作要求. 任何返
回的 preparedStatement 都是針對這個(gè)連接的.
如果我們要利用緩沖區的優(yōu)勢, 要使用和前面相同的規則. 我們需要使用參數話(huà)的
查詢(xún), 這樣 它們就會(huì )和已經(jīng)在緩沖區的某一個(gè)匹配. 大多數應用程序服務(wù)器都允許
你調整緩沖區的大小.
概要
總之, 對于預先準備語(yǔ)句, 我們應該使用參數化的查詢(xún). 這樣允許數據庫重用已經(jīng)存
在的訪(fǎng)問(wèn) 方案, 從而減輕數據庫的負擔. 這樣的緩沖區是這個(gè)數據庫范圍的, 所以
你可以安排你所有的 應用程序, 使用相似的參數化的 SQL, 就會(huì )提高這樣的緩沖區
方案的效率, 因為一個(gè)應用程序 可以使用另一個(gè)應用程序的語(yǔ)句. 一個(gè)應用服務(wù)器
的優(yōu)勢也在于此, 因為訪(fǎng)問(wèn)數據庫的邏輯應該 集中在數據訪(fǎng)問(wèn)層上(OR 映射, 實(shí)體
bean 或者直接 JDBC).
最后, 預先準備語(yǔ)句的正確使用也讓你利用應用程序服務(wù)器的預先準備語(yǔ)句的緩沖
區的好處. 會(huì )提高你的應用程序的性能, 因為應用程序通過(guò)對以前的預先準備語(yǔ)句
的重用減少 JDBC 驅動(dòng)程序調用的次數. 這樣使它能和胖客戶(hù)端的效率競爭, 并且
去掉了不能保持一個(gè)長(cháng)期 連接的壞處.
如果你使用參數化的預先準備語(yǔ)句, 就可以提高數據庫和你的服務(wù)器端的代碼的效
率. 這些提高 都會(huì )允許你的應用程序提高性能.