關於Java Database Connectivity您不知道的5件事:提升您和JDBC API的關系
目前,許多開發人員把 Java Database Connectivity (JDBC) API 當作一種數據訪問平台,比如 Hibernate 或 SpringMany。然而 JDBC 在數據庫連接中不僅僅充當後台角色。對於 JDBC,您了解的越多,您的 RDBMS 交互效率就越高。
在本期 5 件事 系列 中,我將向您介紹幾種 JDBC 2.0 到 JDBC 4.0 中新引入的功能。設計時考慮到現代軟件開發所面臨的挑戰,這些新特性支持應用程序可伸縮性,並提高開發人員的工作效率 — 這是現代 Java 開發人員面臨的兩個最常見的挑戰。
1. 標量函數
不同的 RDBMS 實現對 SQL 和/或增值特性(目的是讓程序員的工作更為簡單)提供不規則的支持。例如,眾所周知,SQL 支持一個標量運算 COUNT(),返回滿足特定 SQL 過濾規則的行數(更確切地說,是 WHERE 謂詞)。除此之外,修改 SQL 返回的值是很棘手的 — 想要從數據庫獲取當前日期和時間會使 JDBC 開發人員、甚至最有耐心的程序員發瘋(甚至是心力憔悴)。
於是,JDBC 規范針對不同的 RDBMS 實現通過標量函數提供一定程度的隔離/改寫。JDBC 規范包括一系列受支持的操作,JDBC 驅動程序應該根據特定數據庫實現的需要進行識別和改寫。因此,對於一個支持返回當前日期和/或時間的數據庫,時間查詢應當如清單 1 那樣簡單:
清單 1. 當前時間?
Connection conn = ...; // get it from someplace
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("{CURRENT_DATE()}");
JDBC API 識別的標量函數完整列表在 JDBC 規范附錄中給出,但是給定的驅動程序或數據庫可能不支持完整列表。您可以使用從 Connection 返回的 DatabaseMetaData 對象來獲取給定 JDBC 支持的函數,如清單 2 所示:
清單 2. 能為我提供什麼?
Connection conn = ...; // get it from someplace
DatabaseMetaData dbmd = conn.getMetaData();
標量函數列表是從各種 DatabaseMetaData 方法返回的一個逗號分隔的 String。例如,所有數值標量由 getNumericFunctions() 調用列出,在結果上執行一個 String.split() — 瞧! — 即刻出現 equals()-testable 列表。
2. 可滾動 ResultSets
創建一個 Connection 對象,並用它來創建一個 Statement,這在 JDBC 中是最常用的。提供給 SQL SELECT 的 Statement 返回一個 ResultSet。然後,通過一個 while 循環(和 Iterator 沒什麼不同)得到 ResultSet,直到 ResultSet 為空,循環體從左到右的每次提取一列。
這整個操作過程是如此普遍,近乎神聖:它這樣做只是因為它應該這樣做。唉!實際上這是完全沒必要的。
引入可滾動 ResultSet
許多開發人員沒有意識到,在過去的幾年中 JBDC 已經有了相當大的增強,盡管這些增強在新版本中已經有所反映。 第一次重大增強是在 JDBC 2.0 中,發生在使用 JDK 1.2 期間。寫這篇文章時,JDBC 已經發展到了 JDBC 4.0。
JDBC 2.0 中一個有趣的增強(盡管常常被忽略)是 ResultSet 的滾動功能,這意味著您可以根據需要前進或者後退,或者兩者均可。這樣做需要一點前瞻性,然而 — JDBC 調用必須指出在創建 Statement 時需要一個可以滾動的 ResultSet。
驗證 ResultSet 類型
如果您懷疑一個驅動程序事實上可能不支持可滾動的 ResultSets,不管 DatabaseMetaData 中是如何寫的,您都要調用 getType() 來驗證 ResultSet 類型。當然,如果您是個偏執的人,您可能也不相信 getType() 的返回值。可以這樣說,如果 getType() 隱瞞關於 ResultSet 的返回值,它們確實是 要吃定您。
如果底層 JDBC 驅動程序支持滾動,一個可滾動的 ResultSet 將從那個 Statement 返回。但是在請求它之前最好弄清楚驅動程序是否支持可滾動性。您可以通過 DatabaseMetaData 對象探詢滾動性,如上所述,這個對象可從任何 Connection 中獲取。
一旦您有了一個 DatabaseMetaData 對象,一個對 getJDBCMajorVersion() 的調用將會確定驅動程序是否支持 JDBC 規范,至少是 JDBC 2.0 規范。當然一個驅動程序可能會隱瞞它對給定規范的支持程度,因此為了安全起見,用期望得到的 ResultSet 類型調用 supportsResultSetType() 方法。(在 ResultSet 類上它是一個常量;稍後我們將對其每個值進行討論。)
清單 3. 可以滾動?
int JDBCVersion = dbmd.getJDBCMajorVersion();
boolean srs = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
if (JDBCVersion > 2 || srs == true)
{
// scroll, baby, scroll!
}
請求一個可滾動的 ResultSet
假設您的驅動程序回答 “是”(如果不是,您需要一個新的驅動程序或數據庫),您可以通過傳遞兩個參數到 Connection.createStatement() 調用來請求一個可滾動的 ResultSet,如清單 4 所示:
清單 4. 我想要滾動!
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
在調用 createStatement() 時,您必須特別小心,因為它的第一個和第二個參數都是 int 的。(在 Java 5 之前我們不能使用枚舉類型!)任何 int 值(包括錯誤的常量)對 createStatement() 都有效。
第一個參數,指定 ResultSet 中期望得到的 “可滾動性”,應該是以下 3 個值之一:
ResultSet.TYPE_FORWARD_ONLY:這是默認的,是我們了解且喜歡的流水游標。
ResultSet.TYPE_SCROLL_INSENSITIVE:這個 ResultSet 支持向後迭代以及向前迭代,但是,如果數據庫中的數據發生變化,ResultSet 將不能反映出來。這個可滾動的 ResultSet 可能是最常用到的類型。
ResultSet.TYPE_SCROLL_SENSITIVE:所創建的 ResultSet 不但支持雙向迭代,而且當數據庫中的數據發生變化時還為您提供一個 “實時” 數據視圖。
第二個參數在下一個技巧中介紹,稍等片刻。
定向滾動
當您從 Statement 獲取一個 ResultSet 後,通過它向後滾動只需調用 previous(),即向後滾動一行,而不是向前,就像 next() 那樣。您也可以調用 first() 返回到 ResultSet 開頭,或者調用 last() 轉到 ResultSet 的末尾,或者...您自己拿主意。
relative() 和 absolute() 方法也是很有用的:前者移動指定數量的行(如果是正數則向前移動,是負數則向後移動),後者移動 ResultSet 中指定數量的行,不管游標在哪。當然,目前行數是由 getRow() 獲取的。
如果您打算通過調用 setFetchDirection() 在一個特定方向進行一些滾動,可以通過指定方向來幫助 ResultSet。(無論向哪個方向滾動,對於 ResultSet 都可行,但是預先知道滾動方向可以優化其數據檢索。)
3. 可更新的 ResultSets
JDBC 不僅僅支持雙向 ResultSet,也支持就地更新 ResultSet。這就是說,與其創建一個新 SQL 語句來修改目前存儲在數據庫中的值,您只需要修改保存在 ResultSet 中的值,之後該值會被自動發送到數據庫中該行所對應的列。
請求一個可更新的 ResultSet 類似於請求一個可滾動的 ResultSet 的過程。事實上,在此您將為 createStatement() 使用第二個參數。您不需要為第二個參數指定 ResultSet.CONCUR_READ_ONLY,只需要發送 ResultSet.CONCUR_UPDATEABLE 即可,如清單 5 所示:
清單 5. 我想要一個可更新的 ResultSet
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
假設您的驅動程序支持可更新光標(這是 JDBC 2.0 規范的另一個特性,這是大多數 “現實” 數據庫所支持的),您可以更新 ResultSet 中任何給定的值,方法是導航到該行並調用它的一個 update...() 方法(如清單 6 所示),如同 ResultSet 的 get...()方法。在 ResultSet 中 update...() 對於實際的列類型是超負荷的。因此要更改名為 “PRICE” 的浮點列,調用 updateFloat("PRICE")。然而,這樣做只能更新 ResultSet 中的值。為了將該值插入支持它的數據庫中,可以調用 updateRow()。如果用戶改變調整價格的想法,調用 cancelRowUpdates() 可以停止所有正在進行的更新。
清單 6. 一個更好的方法
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS =
stmt.executeQuery("SELECT * FROM lineitem WHERE id=1");
scrollingRS.first();
scrollingRS.udpateFloat("PRICE", 121.45f);
// ...
if (userSaidOK)
scrollingRS.updateRow();
else
scrollingRS.cancelRowUpdates();
JDBC 2.0 不只支持更新。如果用戶想要添加一個全新的行,不需要創建一個新 Statement 並執行一個 INSERT,只需要調用 moveToInsertRow(),為每個列調用 update...(),然後調用 insertRow() 完成工作。如果沒有指定一個列值,數據庫會默認將其看作 SQL NULL(如果數據庫模式不允許該列為 NULL,這可能觸發 SQLException)。
當然,如果 ResultSet 支持更新一行,也必然支持通過 deleteRow() 刪除一行。
差點忘了強調一點,所有這些可滾動性和可更新性都適用於 PreparedStatement(通過向 prepareStatement() 方法傳遞參數),由於一直處於 SQL 注入攻擊的危險中,這比一個規則的 Statement 好很多。
4. Rowsets
既然所有這些功能在 JDBC 中大約有 10 年了,為什麼大多數開發人員仍然迷戀向前滾動 ResultSet 和不連貫訪問?
罪魁禍首是可伸縮性。保持最低的數據庫連接是支持大量用戶通過 Internet 訪問公司網站的關鍵。因為滾動和/或更新 ResultSet 通常需要一個開放的網絡連接,而許多開發人員通常不(或不能)使用這些連接。
幸好,JDBC 3.0 引入另一種解決方案讓您同樣可以做很多之前使用 ResultSet 方可以做的事情,而不需要數據庫連接保持開放狀態。
從概念上講,Rowset 本質上是一個 ResultSet,但是它支持連接模型或斷開模型,您所需要做的是創建一個 Rowset,將其指向一個 ResultSet,當它完成自我填充之後,將其作為一個 ResultSet,如清單 7 所示:
清單 7. Rowset 取代 ResultSet
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
if (wantsConnected)
JdbcRowSet rs = new JdbcRowSet(scrollingRS); // connected
else
CachedRowSet crs = new CachedRowSet(scrollingRS); disconnected
JDBC 還附帶了 5 個 Rowset 接口 “實現”(也就是擴展接口)。JdbcRowSet 是一個連接的 Rowset 實現;其余 4 個是斷開的:
CachedRowSet 只是一個斷開的 Rowset.
WebRowSet 是 CachedRowSet 的一個子集,知道如何將其結果轉換成 XML,並再次轉換回來。
JoinRowSet 是一個 WebRowSet,知道如何形成一個 SQL JOIN,而無需連接到數據庫。
FilteredRowSet 是一個 WebRowSet,知道如何更進一步過濾傳遞回來的數據,而不需要連接到數據庫。
Rowsets 是完整的 JavaBeans,意味著它們支持偵聽類事件,因此,如果需要,也可以捕捉、檢查並執行對 Rowset 的任何修改。事實上,如果 Rowset 有自己的 Username、Password、URL 和 DatasourceName 屬性集(這意味著它將使用 DriverManager.getConnection() 創建一個連接)或者 Datasource 屬性集(這很可能由 JNDI 獲取),它甚至能管理對數據庫的全部操作。然後,您可以在 Command 屬性中指定要執行的 SQL,調用 execute(),然後處理結果 — 不需要更多的工作。
通常,Rowset 實現是由 JDBC 驅動程序提供的,因此實際的名稱和/或包由您所使用的 JDBC 驅動程序決定。從 Java 5 開始 Rowset 實現已經是標准版本(standard distribution)的一部分了,因此您只需要創建一個 ...RowsetImpl(),然後讓其運行。
5. 批量更新
盡管 Rowset 很實用,但有時候也不能滿足您的需求,您可能需要返回來直接編寫 SQL 語句。在這種情況下,特別是當您面對一大堆工作時,您就會很感激批量更新功能,可在一個網絡往返行程中在數據庫中執行多條 SQL 語句。
要確定 JDBC 驅動程序是否支持批量更新,快速調用 DatabaseMetaData.supportsBatchUpdates() 可產生一個明示支持與否的布爾值。在支持批量更新時(由一些非 SELECT 標示),所有任務逐個排隊然後在某一瞬間同時得到更新,如清單 8 所示:
清單 8. 讓數據庫進行批量更新!
conn.setAutoCommit(false);
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO lineitems VALUES(?,?,?,?)");
pstmt.setInt(1, 1);
pstmt.setString(2, "52919-49278");
pstmt.setFloat(3, 49.99);
pstmt.setBoolean(4, true);
pstmt.addBatch();
// rinse, lather, repeat
int[] updateCount = pstmt.executeBatch();
conn.commit();
conn.setAutoCommit(true);
默認必須調用 setAutoCommit(),驅動程序會試圖交付提供給它的每條語句。除此之外,其余代碼都是簡單易懂的:使用 Statement 或 PreparedStatement 進行常見 SQL 操作,但是不調用 execute(),而調用 executeBatch(),排隊等候調用而不是立即發送。
准備好各種語句之後,在數據庫中使用 executeBatch() 觸發所有的語句,這將返回一組整型值,每個值保存同樣的結果,好像使用了 executeUpdate() 一樣。
在批量處理的一條語句發生錯誤的情況下,如果驅動程序不支持批量更新,或者批處理中的一條語句返回 ResultSet,驅動程序將拋出一個 BatchUpdateException。有時候,在拋出一個異常之後,驅動程序可能試著繼續執行語句。JDBC 規范不能授權某一行為,因此您應該事先試用驅動程序,這樣就可以確切地知道它是如何工作的。(當然,您要執行單元測試,確保在錯誤成為問題之前發現它,對吧?)
結束語
作為 Java 開發的一個主題,JDBC API 是每個開發人員應該熟知的,就像您的左右手那樣。有趣的是,在過去的幾年中,許多開發人員並不了解 API 的增強功能,因此,他們錯失了本文所講到的省時技巧。
當然,您是否決定使用 JDBC 的新功能取決於您自己。需要考慮的一個關鍵因素是您所使用的系統的可伸縮性。對伸縮性的要求越高,對數據庫的使用就越受限制,因此而減少的網絡流量就會越多。Rowset、標量調用和批量更新將會是給您帶來幫助的益友。另外,嘗試可滾動和可更新的 ResultSet(這不像 Rowset 那樣耗內存),並度量可伸縮性。這可能沒您想象的糟糕。
5 件事 系列 的下一期主題是: 命令行標志。