消除JDBC的瓶頸
摘要
大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它類型的Java應用都需要與數據庫進行交互。與數據庫進行交互需要反復地調用SQL語句、連接管理、事務生命周期、結果處理和異常處理。這些操作都是很常見的;不過這個重復的使用并不是必定需要的。在這篇文章中,我們將介紹一個靈活的架構,它可以解決與一個兼容JDBC的數據庫的重復交互問題。
最近在為公司開發一個小的J2EE應用時,我對執行和處理SQL調用的過程感到很麻煩。我認為在Java開發者中一定有人已經開發了一個架構來消除這個流程。不過,搜索諸如"Java SQL framework" 或者 "JDBC [Java Database Connectivity] framework"等都沒有得到滿意的結果。
問題的提出?
在講述一個解決方法之前,我們先將問題描述一下。如果你要通過一個JDBC數據源執行SQL指令時,你通常需要做些什么呢?
1、建立一個SQL字符串
2、得到一個連接
3、得到一個預處理語句(prepared statement)
4、將值組合到預處理語句中
5、執行語句
6、遍歷結果集并且形成結果對象
還有,你必須考慮那些不斷產生的SQLExceptions;如果這些步驟出現不同的地方,SQLExecptions的開銷就會復合在一起,因為你必須使用多個try/catch塊。
不過,如果我們仔細地觀察一下這些步驟,就可以發現這個過程中有幾個部分在執行期間是不變的:你通常都使用同一個方式來得到一個連接和一個預處理語句。組合預處理語句的方式通常也是一樣的,而執行和處理查詢則是特定的。你可以在六個步驟中提取中其中三個。即使在有點不同的步驟中,我們也可以在其中提取出公共的功能。但是我們應該怎樣自動化及簡化這個過程呢?
查詢架構
我們首先定義一些方法的簽名,這些方法是我們將要用來執行一個SQL語句的。要注意讓它保持簡單,只傳送需要的變量,我們可以編寫一些類似下面簽名的方法:
| public Object[] executeQuery(String sql, Object[] pStmntValues, ResultProcessor processor); |
我們知道在執行期間有所不同的方面是SQL語句、預處理語句的值和結果集是如何分析的。很明顯,sql參數指的是SQL語句。pStmntValues對象數據包含有必須插入到預處理語句中的值,而processor參數則是處理結果集并且返回結果對象的一個對象;我將在后面更詳細地討論這個對象。
在這樣一個方法簽名中,我們就已經將每個JDBC數據庫交互中三個不變的部分隔離開來。現在讓我們討論exeuteQuery()及其它支持的方法,它們都是SQLProcessor類的一部分:
| public class SQLProcessor { public Object[] executeQuery(String sql, Object[] pStmntValues, ResultProcessor processor) { //Get a connection (assume it's part of a ConnectionManager class) Connection conn = ConnectionManager.getConnection(); //Hand off our connection to the method that will actually execute //the call Object[] results = handleQuery(sql, pStmntValues, processor, conn); //Close the connection closeConn(conn); //And return its results return results; } protected Object[] handleQuery(String sql, Object[] pStmntValues, ResultProcessor processor, Connection conn) { //Get a prepared statement to use PreparedStatement stmnt = null; try { //Get an actual prepared statement stmnt = conn.prepareStatement(sql); //Attempt to stuff this statement with the given values. If //no values were given, then we can skip this step. if(pStmntValues != null) { PreparedStatementFactory.buildStatement(stmnt, pStmntValues); } //Attempt to execute the statement ResultSet rs = stmnt.executeQuery(); //Get the results from this query Object[] results = processor.process(rs); //Close out the statement only. The connection will be closed by the //caller. closeStmnt(stmnt); //Return the results return results; //Any SQL exceptions that occur should be recast to our runtime query //exception and thrown from here } catch(SQLException e) { String message = "Could not perform the query for " + sql; //Close out all resources on an exception closeConn(conn); closeStmnt(stmnt); //And rethrow as our runtime exception throw new DatabaseQueryException(message); } } } ... } |
在這些方法中,有兩個部分是不清楚的:PreparedStatementFactory.buildStatement() 和 handleQuery()'s processor.process()方法調用。buildStatement()只是將參數對象數組中的每個對象放入到預處理語句中的相應位置。例如:
| ... //Loop through all objects of the values array, and set the value //of the prepared statement using the value array index for(int i = 0; i < values.length; i++) { //If the object is our representation of a null value, then handle it separately if(value instanceof NullSQLType) { stmnt.setNull(i + 1, ((NullSQLType) value).getFieldType()); } else { stmnt.setObject(i + 1, value); } } |
由于stmnt.setObject(int index, Object value)方法不可以接受一個null對象值,因此我們必須使用自己特殊的構造:NullSQLType類。NullSQLType表示一個null語句的占位符,并且包含有該字段的JDBC類型。當一個NullSQLType對象實例化時,它獲得它將要代替的字段的SQL類型。如上所示,當預處理語句通過一個NullSQLType組合時,你可以使用NullSQLType的字段類型來告訴預處理語句該字段的JDBC類型。這就是說,你使用NullSQLType來表明正在使用一個null值來組合一個預處理語句,并且通過它存放該字段的JDBC類型。
現在我已經解釋了PreparedStatementFactory.buildStatement()的邏輯,我將解釋另一個缺少的部分:processor.process()。processor是ResultProcessor類型,這是一個接口,它表示由查詢結果集建立域對象的類。ResultProcessor包含有一個簡單的方法,它返回結果對象的一個數組:
| public interface ResultProcessor { public Object[] process(ResultSet rs) throws SQLException; } |
一個典型的結果處理器遍歷給出的結果集,并且由結果集合的行中形成域對象/對象結構。現在我將通過一個現實世界中的例子來綜合講述一下。
查詢例子
你經常都需要利用一個用戶的信息表由數據庫中得到一個用戶的對象,假設我們使用以下的USERS表:
| USERS table Column Name Data Type ID NUMBER USERNAME VARCHAR F_NAME VARCHAR L_NAME VARCHAR EMAIL VARCHAR |
并且假設我們擁有一個User對象,它的構造器是:
| public User(int id, String userName, String firstName, String lastName, String email) |
如果我們沒有使用這篇文章講述的架構,我們將需要一個頗大的方法來處理由數據庫中接收用戶信息并且形成User對象。那么我們應該怎樣利用我們的架構呢?
首先,我們構造SQL語句:
| private static final String SQL_GET_USER = "SELECT * FROM USERS WHERE ID = ?"; |
接著,我們形成ResultProcessor,我們將使用它來接受結果集并且形成一個User對象:
| public class UserResultProcessor implements ResultProcessor { //Column definitions here (i.e., COLUMN_USERNAME, etc...) .. public Object[] process(ResultSet rs) throws SQLException { //Where we will collect all returned users List users = new ArrayList(); User user = null; //If there were results returned, then process them while(rs.next()) { user = new User(rs.getInt(COLUMN_ID), rs.getString(COLUMN_USERNAME), rs.getString(COLUMN_FIRST_NAME), rs.getString(COLUMN_LAST_NAME), rs.getString(COLUMN_EMAIL)); users.add(user); } return users.toArray(new User[users.size()]); |
最后,我們將寫一個方法來執行查詢并且返回User對象:
| public User getUser(int userId) { //Get a SQL processor and execute the query SQLProcessor processor = new SQLProcessor(); Object[] users = processor.executeQuery(SQL_GET_USER_BY_ID, new Object[] {new Integer(userId)}, new UserResultProcessor()); //And just return the first User object return (User) users[0]; } |
這就是全部。我們只需要一個處理類和一個簡單的方法,我們就可以無需進行直接的連接維護、語句和異常處理。此外,如果我們擁有另外一個查詢由用戶表中得到一行,例如通過用戶名或者密碼,我們可以重新使用UserResultProcessor。我們只需要插入一個不同的SQL語句,并且可以重新使用以前方法的用戶處理器。由于返回行的元數據并不依賴查詢,所以我們可以重新使用結果處理器。
更新的架構
那么數據庫更新又如何呢?我們可以用類似的方法處理,只需要進行一些修改就可以了。首先,我們必須增加兩個新的方法到SQLProcessor類。它們類似executeQuery()和handleQuery()方法,除了你無需處理結果集,你只需要將更新的行數作為調用的結果:
| public void executeUpdate(String sql, Object[] pStmntValues, UpdateProcessor processor) { //Get a connection Connection conn = ConnectionManager.getConnection(); //Send it off to be executed handleUpdate(sql, pStmntValues, processor, conn); //Close the connection closeConn(conn); } protected void handleUpdate(String sql, Object[] pStmntValues, UpdateProcessor processor, Connection conn) { //Get a prepared statement to use PreparedStatement stmnt = null; try { //Get an actual prepared statement stmnt = conn.prepareStatement(sql); //Attempt to stuff this statement with the given values. If //no values were given, then we can skip this step. if(pStmntValues != null) { PreparedStatementFactory.buildStatement(stmnt, pStmntValues); } //Attempt to execute the statement int rows = stmnt.executeUpdate(); //Now hand off the number of rows updated to the processor processor.process(rows); //Close out the statement only. The connection will be closed by the //caller. closeStmnt(stmnt); //Any SQL exceptions that occur should be recast to our runtime query //exception and thrown from here } catch(SQLException e) { String message = "Could not perform the update for " + sql; //Close out all resources on an exception closeConn(conn); closeStmnt(stmnt); //And rethrow as our exception throw new DatabaseUpdateException(message); } } |
這些方法和查詢處理方法的區別僅在于它們是如何處理調用的結果:由于一個更新的操作只返回更新的行數,因此我們無需結果處理器。我們也可以忽略更新的行數,不過有時我們可能需要確認一個更新的產生。UpdateProcessor獲得更新行的數據,并且可以對行的數目進行任何類型的確認或者記錄:
| public interface UpdateProcessor { public void process(int rows); } |
如果一個更新的調用必須至少更新一行,這樣實現UpdateProcessor的對象可以檢查更新的行數,并且可以在沒有行被更新的時候拋出一個特定的異常。或者,我們可能需要記錄下更新的行數,初始化一個結果處理或者觸發一個更新的事件。你可以將這些需求的代碼放在你定義的UpdateProcessor中。你應該知道:各種可能的處理都是存在的,并沒有任何的限制,可以很容易得集成到架構中。