明輝手游網(wǎng)中心:是一個免費提供流行視頻軟件教程、在線學習分享的學習平臺!

消除JDBC的瓶頸

[摘要]摘要 大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它類型的Java應用都需要與數(shù)據(jù)庫進行交互。與數(shù)據(jù)庫進行交互需要反復地調用SQL語句、連接管理、事務生命周期、結果處理和異常處理。這些操作都是很常見的;不過這個重復的使用并不是必定需要的。在這...
摘要

大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它類型的Java應用都需要與數(shù)據(jù)庫進行交互。與數(shù)據(jù)庫進行交互需要反復地調用SQL語句、連接管理、事務生命周期、結果處理和異常處理。這些操作都是很常見的;不過這個重復的使用并不是必定需要的。在這篇文章中,我們將介紹一個靈活的架構,它可以解決與一個兼容JDBC的數(shù)據(jù)庫的重復交互問題。

最近在為公司開發(fā)一個小的J2EE應用時,我對執(zhí)行和處理SQL調用的過程感到很麻煩。我認為在Java開發(fā)者中一定有人已經(jīng)開發(fā)了一個架構來消除這個流程。不過,搜索諸如\"Java SQL framework" 或者 "JDBC [Java Database Connectivity] framework"等都沒有得到滿意的結果。

問題的提出?

在講述一個解決方法之前,我們先將問題描述一下。如果你要通過一個JDBC數(shù)據(jù)源執(zhí)行SQL指令時,你通常需要做些什么呢?

1、建立一個SQL字符串

2、得到一個連接

3、得到一個預處理語句(prepared statement)

4、將值組合到預處理語句中

5、執(zhí)行語句\r

6、遍歷結果集并且形成結果對象\r

還有,你必須考慮那些不斷產(chǎn)生的SQLExceptions;如果這些步驟出現(xiàn)不同的地方,SQLExecptions的開銷就會復合在一起,因為你必須使用多個try/catch塊。

不過,如果我們仔細地觀察一下這些步驟,就可以發(fā)現(xiàn)這個過程中有幾個部分在執(zhí)行期間是不變的:你通常都使用同一個方式來得到一個連接和一個預處理語句。組合預處理語句的方式通常也是一樣的,而執(zhí)行和處理查詢則是特定的。你可以在六個步驟中提取中其中三個。即使在有點不同的步驟中,我們也可以在其中提取出公共的功能。但是我們應該怎樣自動化及簡化這個過程呢?

查詢架構

我們首先定義一些方法的簽名,這些方法是我們將要用來執(zhí)行一個SQL語句的。要注意讓它保持簡單,只傳送需要的變量,我們可以編寫一些類似下面簽名的方法:

public Object[] executeQuery(String sql, Object[] pStmntValues,ResultProcessor processor);

我們知道在執(zhí)行期間有所不同的方面是SQL語句、預處理語句的值和結果集是如何分析的。很明顯,sql參數(shù)指的是SQL語句。pStmntValues對象數(shù)據(jù)包含有必須插入到預處理語句中的值,而processor參數(shù)則是處理結果集并且返回結果對象的一個對象;我將在后面更詳細地討論這個對象。

在這樣一個方法簽名中,我們就已經(jīng)將每個JDBC數(shù)據(jù)庫交互中三個不變的部分隔離開來,F(xiàn)在讓我們討論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 callObject[] results = handleQuery(sql, pStmntValues, processor, conn);//Close the connectioncloseConn(conn);//And return its resultsreturn results;}protected Object[] handleQuery(String sql, Object[] pStmntValues,ResultProcessor processor, Connection conn) {//Get a prepared statement to usePreparedStatement stmnt = null;try {//Get an actual prepared statementstmnt = 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 statementResultSet rs = stmnt.executeQuery();//Get the results from this queryObject[] results = processor.process(rs);//Close out the statement only. The connection will be closed by the//caller.closeStmnt(stmnt);//Return the resultsreturn 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 exceptioncloseConn(conn);closeStmnt(stmnt);//And rethrow as our runtime exceptionthrow new DatabaseQueryException(message);}}}...}

在這些方法中,有兩個部分是不清楚的:PreparedStatementFactory.buildStatement() 和 handleQuery()'s processor.process()方法調用。buildStatement()只是將參數(shù)對象數(shù)組中的每個對象放入到預處理語句中的相應位置。例如:

...//Loop through all objects of the values array, and set the value//of the prepared statement using the value array indexfor(int i = 0; i < values.length; i++) {//If the object is our representation of a null value, then handle it separatelyif(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類型。

現(xiàn)在我已經(jīng)解釋了PreparedStatementFactory.buildStatement()的邏輯,我將解釋另一個缺少的部分:processor.process()。processor是ResultProcessor類型,這是一個接口,它表示由查詢結果集建立域對象的類。ResultProcessor包含有一個簡單的方法,它返回結果對象的一個數(shù)組:

public interface ResultProcessor {public Object[] process(ResultSet rs) throws SQLException;}

一個典型的結果處理器遍歷給出的結果集,并且由結果集合的行中形成域對象/對象結構,F(xiàn)在我將通過一個現(xiàn)實世界中的例子來綜合講述一下。

查詢例子

你經(jīng)常都需要利用一個用戶的信息表由數(shù)據(jù)庫中得到一個用戶的對象,假設我們使用以下的USERS表:

USERS tableColumn 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)

如果我們沒有使用這篇文章講述的架構,我們將需要一個頗大的方法來處理由數(shù)據(jù)庫中接收用戶信息并且形成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 usersList users = new ArrayList();User user = null;//If there were results returned, then process themwhile(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()]);

最后,我們將寫一個方法來執(zhí)行查詢并且返回User對象:

public User getUser(int userId) {//Get a SQL processor and execute the querySQLProcessor 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 objectreturn (User) users[0];}

這就是全部。我們只需要一個處理類和一個簡單的方法,我們就可以無需進行直接的連接維護、語句和異常處理。此外,如果我們擁有另外一個查詢由用戶表中得到一行,例如通過用戶名或者密碼,我們可以重新使用UserResultProcessor。我們只需要插入一個不同的SQL語句,并且可以重新使用以前方法的用戶處理器。由于返回行的元數(shù)據(jù)并不依賴查詢,所以我們可以重新使用結果處理器。

更新的架構

那么數(shù)據(jù)庫更新又如何呢?我們可以用類似的方法處理,只需要進行一些修改就可以了。首先,我們必須增加兩個新的方法到SQLProcessor類。它們類似executeQuery()和handleQuery()方法,除了你無需處理結果集,你只需要將更新的行數(shù)作為調用的結果:

public void executeUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor) {//Get a connectionConnection conn = ConnectionManager.getConnection();//Send it off to be executedhandleUpdate(sql, pStmntValues, processor, conn);//Close the connectioncloseConn(conn);}protected void handleUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor, Connection conn) {//Get a prepared statement to usePreparedStatement stmnt = null;try {//Get an actual prepared statementstmnt = 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 statementint rows = stmnt.executeUpdate();//Now hand off the number of rows updated to the processorprocessor.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 exceptioncloseConn(conn);closeStmnt(stmnt);//And rethrow as our exceptionthrow new DatabaseUpdateException(message);}}

這些方法和查詢處理方法的區(qū)別僅在于它們是如何處理調用的結果:由于一個更新的操作只返回更新的行數(shù),因此我們無需結果處理器。我們也可以忽略更新的行數(shù),不過有時我們可能需要確認一個更新的產(chǎn)生。UpdateProcessor獲得更新行的數(shù)據(jù),并且可以對行的數(shù)目進行任何類型的確認或者記錄:

public interface UpdateProcessor {public void process(int rows);}

如果一個更新的調用必須至少更新一行,這樣實現(xiàn)UpdateProcessor的對象可以檢查更新的行數(shù),并且可以在沒有行被更新的時候拋出一個特定的異常;蛘,我們可能需要記錄下更新的行數(shù),初始化一個結果處理或者觸發(fā)一個更新的事件。你可以將這些需求的代碼放在你定義的UpdateProcessor中。你應該知道:各種可能的處理都是存在的,并沒有任何的限制,可以很容易得集成到架構中。
更新的例子

我將繼續(xù)使用上面解釋的User模型來講述如何更新一個用戶的信息:

首先,構造SQL語句:

private static final String SQL_UPDATE_USER = "UPDATE USERS SET USERNAME = ?, " +"F_NAME = ?, " +"L_NAME = ?, " +"EMAIL = ? " +"WHERE ID = ?";

接著,構造UpdateProcessor,我們將用它來檢驗更新的行數(shù),并且在沒有行被更新的時候拋出一個異常:

public class MandatoryUpdateProcessor implements UpdateProcessor {public void process(int rows) {if(rows < 1) {String message = "There were no rows updated as a result of this operation.";throw new IllegalStateException(message);}}}

最后就寫編寫執(zhí)行更新的方法:

public static void updateUser(User user) {SQLProcessor sqlProcessor = new SQLProcessor();//Use our get user SQL statementsqlProcessor.executeUpdate(SQL_UPDATE_USER,new Object[] {user.getUserName(),user.getFirstName(),user.getLastName(),user.getEmail(),new Integer(user.getId())},new MandatoryUpdateProcessor());

如前面的例子一樣,我們無需直接處理SQLExceptions和Connections就執(zhí)行了一個更新的操作。

事務\r

前面已經(jīng)說過,我對其它的SQL架構實現(xiàn)都不滿意,因為它們并不擁有預定義語句、獨立的結果集處理或者可處理事務。我們已經(jīng)通過buildStatement() 的方法解決了預處理語句的問題,還有不同的處理器(processors)已經(jīng)將結果集的處理分離出來。不過還有一個問題,我們的架構如何處理事務呢?

一個事務和一個獨立SQL調用的區(qū)別只是在于在它的生命周期內,它都使用同一個連接,還有,自動提交標志也必須設置為off。因為我們必須有一個方法來指定一個事務已經(jīng)開始,并且在何時結束。在整個事務的周期內,它都使用同一個連接,并且在事務結束的時候進行提交。

要處理事務,我們可以重用SQLProcessor的很多方面。為什么將該類的executeUpdate() 和handleUpdate()獨立開來呢,將它們結合為一個方法也很簡單的。我這樣做是為了將真正的SQL執(zhí)行和連接管理獨立開來。在建立事務系統(tǒng)時,我們必須在幾個SQL執(zhí)行期間對連接進行控制,這樣做就方便多了。

為了令事務工作,我們必須保持狀態(tài),特別是連接的狀態(tài)。直到現(xiàn)在,SQLProcessor還是一個無狀態(tài)的類。它缺乏成員變量。為了重用SQLProcessor,我們創(chuàng)建了一個事務封裝類,它接收一個SQLProcessor并且透明地處理事務的生命周期。

具體的代碼是:

public class SQLTransaction {private SQLProcessor sqlProcessor;private Connection conn;//Assume constructor that initializes the connection and sets auto commit to false...public void executeUpdate(String sql, Object[] pStmntValues,UpdateProcessor processor) {//Try and get the results. If an update fails, then rollback//the transaction and rethrow the exception.try {sqlProcessor.handleUpdate(sql, pStmntValues, processor, conn);} catch(DatabaseUpdateException e) {rollbackTransaction();throw e;} }public void commitTransaction() {//Try to commit and release all resourcestry {conn.commit();sqlProcessor.closeConn(conn);//If something happens, then attempt a rollback and release resources} catch(Exception e) {rollbackTransaction();throw new DatabaseUpdateException("Could not commit the current transaction.");}}private void rollbackTransaction() {//Try to rollback and release all resourcestry {conn.rollback();conn.setAutoCommit(true);sqlProcessor.closeConn(conn);//If something happens, then just swallow it} catch(SQLException e) {sqlProcessor.closeConn(conn);}}}

SQLTransaction擁有許多新的方法,但是其中的大部分都是很簡單的,并且只處理連接或者事務處理。在整個事務周期內,這個事務封裝類只是在SQLProcessor中增加了一個簡單的連接管理。當一個事務開始時,它接收一個新的連接,并且將其自動提交屬性設置為false。其后的每個執(zhí)行都是使用同一個連接(傳送到SQLProcessor的handleUpdate()方法中),因此事務保持完整。

只有當我們的持久性對象或者方法調用commitTransaction()時,事務才被提交,并且關閉連接。如果在執(zhí)行期間發(fā)生了異常,SQLTransaction可以捕捉該異常,自動進行回滾,并且拋出異常。

事務例子

讓我們來看一個簡單的事務\r

//Reuse the SQL_UPDATE_USER statement defined abovepublic static void updateUsers(User[] users) {//Get our transactionSQLTransaction trans = sqlProcessor.startTransaction();//For each user, update itUser user = null;for(int i = 0; i < users.length; i++) {user = users[i];trans.executeUpdate(SQL_UPDATE_USER,new Object[] {user.getUserName(),user.getFirstName(),user.getLastName(),user.getEmail(),new Integer(user.getId())},new MandatoryUpdateProcessor());}//Now commit the transactiontrans.commitTransaction();}

上面為我們展示了一個事務處理的例子,雖然簡單,但我們可以看出它是如何工作的。如果在執(zhí)行executeUpdate()方法調用時失敗,這時將會回滾事務,并且拋出一個異常。調用這個方法的開發(fā)者從不需要擔心事務的回滾或者連接是否已經(jīng)關閉。這些都是在后臺處理的。開發(fā)者只需要關心商業(yè)的邏輯。

事務也可以很輕松地處理一個查詢,不過這里我沒有提及,因為事務通常都是由一系列的更新組成的。

問題\r

在我寫這篇文章的時候,對于這個架構,我提出了一些疑問。這里我將這些問題提出來,因為你們可能也會碰到同樣的問題。

自定義連接

如果每個事務使用的連接不一樣時會如何?如果ConnectionManager需要一些變量來告訴它從哪個連接池得到連接?你可以很容易就將這些特性集合到這個架構中。executeQuery() 和 executeUpdate()方法(屬于SQLProcessor和SQLTransaction類)將需要接收這些自定義的連接參數(shù),并且將他們傳送到ConnectionManager。要記得所有的連接管理都將在執(zhí)行的方法中發(fā)生。

此外,如果更面向對象化一點,連接制造者可以在初始化時傳送到SQLProcessor中。然后,對于每個不同的連接制造者類型,你將需要一個SQLProcessor實例。根據(jù)你連接的可變性,這或許不是理想的做法。

ResultProcessor返回類型

為什么ResultProcessor接口指定了process()方法應該返回一個對象的數(shù)組?為什么不使用一個List?在我使用這個架構來開發(fā)的大部分應用中,SQL查詢只返回一個對象。如果構造一個List,然后將一個對象加入其中,這樣的開銷較大,而返回一個對象的一個數(shù)組是比較簡單的。不過,如果在你的應用中需要使用對象collections,那么返回一個List更好。

SQLProcessor初始管理\r

在這篇文章的例子中,對于必須執(zhí)行一個SQL調用的每個方法,初始化一個SQLProcessor。由于SQLProcessors完全是沒有狀態(tài)的,所以在調用的方法中將processor獨立出來是很有意義的。

而對于SQLTransaction類,則是缺少狀態(tài)的,因此它不能獨立使用。我建議你為SQLProcessor類增加一個簡單的方法,而不是學習如何初始化一個SQLTransaction,如下所示:

public SQLTransaction startTransaction() {
return new SQLTransaction(this);
}

這樣就會令全部的事務功能都在SQLProcessor類中訪問到,并且限制了你必須知道的方法調用。

數(shù)據(jù)庫異常

我使用了幾種不同類型的數(shù)據(jù)庫異常將全部可能在運行時發(fā)生的SQLExceptions封裝起來。在我使用該架構的應用中,我發(fā)現(xiàn)將這些異常變成runtime exceptions更為方便,所以我使用了一個異常處理器。你可能認為這些異常應該聲明,這樣它們可以盡量在錯誤的發(fā)生點被處理。不過,這樣就會令SQL異常處理的流程和以前的SQLExceptions一樣,這種情況我們是盡量避免的。

省心的JDBC programming

這篇文章提出的架構可以令查詢、更新和事務執(zhí)行的操作更加簡單。在類似的SQL調用中,你只需要關注可重用的支持類中的一個方法。我的希望是該架構可以提高你進行JDBC編程的效率。