消除JDBC的瓶頸
發(fā)表時間:2024-05-26 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]摘要 大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它類型的Java應(yīng)用都需要與數(shù)據(jù)庫進(jìn)行交互。與數(shù)據(jù)庫進(jìn)行交互需要反復(fù)地調(diào)用SQL語句、連接管理、事務(wù)生命周期、結(jié)果處理和異常處理。這些操作都是很常見的;不過這個重復(fù)的使用并不是必定需要的。在這...
摘要
大部分的J2EE(Java 2 Platform, Enterprise Edition)和其它類型的Java應(yīng)用都需要與數(shù)據(jù)庫進(jìn)行交互。與數(shù)據(jù)庫進(jìn)行交互需要反復(fù)地調(diào)用SQL語句、連接管理、事務(wù)生命周期、結(jié)果處理和異常處理。這些操作都是很常見的;不過這個重復(fù)的使用并不是必定需要的。在這篇文章中,我們將介紹一個靈活的架構(gòu),它可以解決與一個兼容JDBC的數(shù)據(jù)庫的重復(fù)交互問題。
最近在為公司開發(fā)一個小的J2EE應(yīng)用時,我對執(zhí)行和處理SQL調(diào)用的過程感到很麻煩。我認(rèn)為在Java開發(fā)者中一定有人已經(jīng)開發(fā)了一個架構(gòu)來消除這個流程。不過,搜索諸如\"Java SQL framework" 或者 "JDBC [Java Database Connectivity] framework"等都沒有得到滿意的結(jié)果。
問題的提出?
在講述一個解決方法之前,我們先將問題描述一下。如果你要通過一個JDBC數(shù)據(jù)源執(zhí)行SQL指令時,你通常需要做些什么呢?
1、建立一個SQL字符串
2、得到一個連接
3、得到一個預(yù)處理語句(prepared statement)
4、將值組合到預(yù)處理語句中
5、執(zhí)行語句\r
6、遍歷結(jié)果集并且形成結(jié)果對象\r
還有,你必須考慮那些不斷產(chǎn)生的SQLExceptions;如果這些步驟出現(xiàn)不同的地方,SQLExecptions的開銷就會復(fù)合在一起,因為你必須使用多個try/catch塊。
不過,如果我們仔細(xì)地觀察一下這些步驟,就可以發(fā)現(xiàn)這個過程中有幾個部分在執(zhí)行期間是不變的:你通常都使用同一個方式來得到一個連接和一個預(yù)處理語句。組合預(yù)處理語句的方式通常也是一樣的,而執(zhí)行和處理查詢則是特定的。你可以在六個步驟中提取中其中三個。即使在有點不同的步驟中,我們也可以在其中提取出公共的功能。但是我們應(yīng)該怎樣自動化及簡化這個過程呢?
查詢架構(gòu)
我們首先定義一些方法的簽名,這些方法是我們將要用來執(zhí)行一個SQL語句的。要注意讓它保持簡單,只傳送需要的變量,我們可以編寫一些類似下面簽名的方法:
public Object[] executeQuery(String sql, Object[] pStmntValues,ResultProcessor processor);
我們知道在執(zhí)行期間有所不同的方面是SQL語句、預(yù)處理語句的值和結(jié)果集是如何分析的。很明顯,sql參數(shù)指的是SQL語句。pStmntValues對象數(shù)據(jù)包含有必須插入到預(yù)處理語句中的值,而processor參數(shù)則是處理結(jié)果集并且返回結(jié)果對象的一個對象;我將在后面更詳細(xì)地討論這個對象。
在這樣一個方法簽名中,我們就已經(jīng)將每個JDBC數(shù)據(jù)庫交互中三個不變的部分隔離開來。現(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()方法調(diào)用。buildStatement()只是將參數(shù)對象數(shù)組中的每個對象放入到預(yù)處理語句中的相應(yīng)位置。例如:
...//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對象值,因此我們必須使用自己特殊的構(gòu)造:NullSQLType類。NullSQLType表示一個null語句的占位符,并且包含有該字段的JDBC類型。當(dāng)一個NullSQLType對象實例化時,它獲得它將要代替的字段的SQL類型。如上所示,當(dāng)預(yù)處理語句通過一個NullSQLType組合時,你可以使用NullSQLType的字段類型來告訴預(yù)處理語句該字段的JDBC類型。這就是說,你使用NullSQLType來表明正在使用一個null值來組合一個預(yù)處理語句,并且通過它存放該字段的JDBC類型。
現(xiàn)在我已經(jīng)解釋了PreparedStatementFactory.buildStatement()的邏輯,我將解釋另一個缺少的部分:processor.process()。processor是ResultProcessor類型,這是一個接口,它表示由查詢結(jié)果集建立域?qū)ο蟮念悺esultProcessor包含有一個簡單的方法,它返回結(jié)果對象的一個數(shù)組:
public interface ResultProcessor {public Object[] process(ResultSet rs) throws SQLException;}
一個典型的結(jié)果處理器遍歷給出的結(jié)果集,并且由結(jié)果集合的行中形成域?qū)ο?對象結(jié)構(gòu)。現(xiàn)在我將通過一個現(xiàn)實世界中的例子來綜合講述一下。
查詢例子
你經(jīng)常都需要利用一個用戶的信息表由數(shù)據(jù)庫中得到一個用戶的對象,假設(shè)我們使用以下的USERS表:
USERS tableColumn Name Data Type ID NUMBER USERNAME VARCHAR F_NAME VARCHAR L_NAME VARCHAR EMAIL VARCHAR
并且假設(shè)我們擁有一個User對象,它的構(gòu)造器是:
public User(int id, String userName, String firstName,
String lastName, String email)
如果我們沒有使用這篇文章講述的架構(gòu),我們將需要一個頗大的方法來處理由數(shù)據(jù)庫中接收用戶信息并且形成User對象。那么我們應(yīng)該怎樣利用我們的架構(gòu)呢?
首先,我們構(gòu)造SQL語句:
private static final String SQL_GET_USER = "SELECT * FROM USERS WHERE ID = ?";
接著,我們形成ResultProcessor,我們將使用它來接受結(jié)果集并且形成一個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];}
這就是全部。我們只需要一個處理類和一個簡單的方法,我們就可以無需進(jìn)行直接的連接維護(hù)、語句和異常處理。此外,如果我們擁有另外一個查詢由用戶表中得到一行,例如通過用戶名或者密碼,我們可以重新使用UserResultProcessor。我們只需要插入一個不同的SQL語句,并且可以重新使用以前方法的用戶處理器。由于返回行的元數(shù)據(jù)并不依賴查詢,所以我們可以重新使用結(jié)果處理器。
更新的架構(gòu)
那么數(shù)據(jù)庫更新又如何呢?我們可以用類似的方法處理,只需要進(jìn)行一些修改就可以了。首先,我們必須增加兩個新的方法到SQLProcessor類。它們類似executeQuery()和handleQuery()方法,除了你無需處理結(jié)果集,你只需要將更新的行數(shù)作為調(diào)用的結(jié)果:
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ū)別僅在于它們是如何處理調(diào)用的結(jié)果:由于一個更新的操作只返回更新的行數(shù),因此我們無需結(jié)果處理器。我們也可以忽略更新的行數(shù),不過有時我們可能需要確認(rèn)一個更新的產(chǎn)生。UpdateProcessor獲得更新行的數(shù)據(jù),并且可以對行的數(shù)目進(jìn)行任何類型的確認(rèn)或者記錄:
public interface UpdateProcessor {public void process(int rows);}
如果一個更新的調(diào)用必須至少更新一行,這樣實現(xiàn)UpdateProcessor的對象可以檢查更新的行數(shù),并且可以在沒有行被更新的時候拋出一個特定的異常。或者,我們可能需要記錄下更新的行數(shù),初始化一個結(jié)果處理或者觸發(fā)一個更新的事件。你可以將這些需求的代碼放在你定義的UpdateProcessor中。你應(yīng)該知道:各種可能的處理都是存在的,并沒有任何的限制,可以很容易得集成到架構(gòu)中。
更新的例子
我將繼續(xù)使用上面解釋的User模型來講述如何更新一個用戶的信息:
首先,構(gòu)造SQL語句:
private static final String SQL_UPDATE_USER = "UPDATE USERS SET USERNAME = ?, " +"F_NAME = ?, " +"L_NAME = ?, " +"EMAIL = ? " +"WHERE ID = ?";
接著,構(gòu)造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í)行了一個更新的操作。
事務(wù)\r
前面已經(jīng)說過,我對其它的SQL架構(gòu)實現(xiàn)都不滿意,因為它們并不擁有預(yù)定義語句、獨立的結(jié)果集處理或者可處理事務(wù)。我們已經(jīng)通過buildStatement() 的方法解決了預(yù)處理語句的問題,還有不同的處理器(processors)已經(jīng)將結(jié)果集的處理分離出來。不過還有一個問題,我們的架構(gòu)如何處理事務(wù)呢?
一個事務(wù)和一個獨立SQL調(diào)用的區(qū)別只是在于在它的生命周期內(nèi),它都使用同一個連接,還有,自動提交標(biāo)志也必須設(shè)置為off。因為我們必須有一個方法來指定一個事務(wù)已經(jīng)開始,并且在何時結(jié)束。在整個事務(wù)的周期內(nèi),它都使用同一個連接,并且在事務(wù)結(jié)束的時候進(jìn)行提交。
要處理事務(wù),我們可以重用SQLProcessor的很多方面。為什么將該類的executeUpdate() 和handleUpdate()獨立開來呢,將它們結(jié)合為一個方法也很簡單的。我這樣做是為了將真正的SQL執(zhí)行和連接管理獨立開來。在建立事務(wù)系統(tǒng)時,我們必須在幾個SQL執(zhí)行期間對連接進(jìn)行控制,這樣做就方便多了。
為了令事務(wù)工作,我們必須保持狀態(tài),特別是連接的狀態(tài)。直到現(xiàn)在,SQLProcessor還是一個無狀態(tài)的類。它缺乏成員變量。為了重用SQLProcessor,我們創(chuàng)建了一個事務(wù)封裝類,它接收一個SQLProcessor并且透明地處理事務(wù)的生命周期。
具體的代碼是:
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擁有許多新的方法,但是其中的大部分都是很簡單的,并且只處理連接或者事務(wù)處理。在整個事務(wù)周期內(nèi),這個事務(wù)封裝類只是在SQLProcessor中增加了一個簡單的連接管理。當(dāng)一個事務(wù)開始時,它接收一個新的連接,并且將其自動提交屬性設(shè)置為false。其后的每個執(zhí)行都是使用同一個連接(傳送到SQLProcessor的handleUpdate()方法中),因此事務(wù)保持完整。
只有當(dāng)我們的持久性對象或者方法調(diào)用commitTransaction()時,事務(wù)才被提交,并且關(guān)閉連接。如果在執(zhí)行期間發(fā)生了異常,SQLTransaction可以捕捉該異常,自動進(jìn)行回滾,并且拋出異常。
事務(wù)例子
讓我們來看一個簡單的事務(wù)\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();}
上面為我們展示了一個事務(wù)處理的例子,雖然簡單,但我們可以看出它是如何工作的。如果在執(zhí)行executeUpdate()方法調(diào)用時失敗,這時將會回滾事務(wù),并且拋出一個異常。調(diào)用這個方法的開發(fā)者從不需要擔(dān)心事務(wù)的回滾或者連接是否已經(jīng)關(guān)閉。這些都是在后臺處理的。開發(fā)者只需要關(guān)心商業(yè)的邏輯。
事務(wù)也可以很輕松地處理一個查詢,不過這里我沒有提及,因為事務(wù)通常都是由一系列的更新組成的。
問題\r
在我寫這篇文章的時候,對于這個架構(gòu),我提出了一些疑問。這里我將這些問題提出來,因為你們可能也會碰到同樣的問題。
自定義連接
如果每個事務(wù)使用的連接不一樣時會如何?如果ConnectionManager需要一些變量來告訴它從哪個連接池得到連接?你可以很容易就將這些特性集合到這個架構(gòu)中。executeQuery() 和 executeUpdate()方法(屬于SQLProcessor和SQLTransaction類)將需要接收這些自定義的連接參數(shù),并且將他們傳送到ConnectionManager。要記得所有的連接管理都將在執(zhí)行的方法中發(fā)生。
此外,如果更面向?qū)ο蠡稽c,連接制造者可以在初始化時傳送到SQLProcessor中。然后,對于每個不同的連接制造者類型,你將需要一個SQLProcessor實例。根據(jù)你連接的可變性,這或許不是理想的做法。
ResultProcessor返回類型
為什么ResultProcessor接口指定了process()方法應(yīng)該返回一個對象的數(shù)組?為什么不使用一個List?在我使用這個架構(gòu)來開發(fā)的大部分應(yīng)用中,SQL查詢只返回一個對象。如果構(gòu)造一個List,然后將一個對象加入其中,這樣的開銷較大,而返回一個對象的一個數(shù)組是比較簡單的。不過,如果在你的應(yīng)用中需要使用對象collections,那么返回一個List更好。
SQLProcessor初始管理\r
在這篇文章的例子中,對于必須執(zhí)行一個SQL調(diào)用的每個方法,初始化一個SQLProcessor。由于SQLProcessors完全是沒有狀態(tài)的,所以在調(diào)用的方法中將processor獨立出來是很有意義的。
而對于SQLTransaction類,則是缺少狀態(tài)的,因此它不能獨立使用。我建議你為SQLProcessor類增加一個簡單的方法,而不是學(xué)習(xí)如何初始化一個SQLTransaction,如下所示:
public SQLTransaction startTransaction() {
return new SQLTransaction(this);
}
這樣就會令全部的事務(wù)功能都在SQLProcessor類中訪問到,并且限制了你必須知道的方法調(diào)用。
數(shù)據(jù)庫異常
我使用了幾種不同類型的數(shù)據(jù)庫異常將全部可能在運(yùn)行時發(fā)生的SQLExceptions封裝起來。在我使用該架構(gòu)的應(yīng)用中,我發(fā)現(xiàn)將這些異常變成runtime exceptions更為方便,所以我使用了一個異常處理器。你可能認(rèn)為這些異常應(yīng)該聲明,這樣它們可以盡量在錯誤的發(fā)生點被處理。不過,這樣就會令SQL異常處理的流程和以前的SQLExceptions一樣,這種情況我們是盡量避免的。
省心的JDBC programming
這篇文章提出的架構(gòu)可以令查詢、更新和事務(wù)執(zhí)行的操作更加簡單。在類似的SQL調(diào)用中,你只需要關(guān)注可重用的支持類中的一個方法。我的希望是該架構(gòu)可以提高你進(jìn)行JDBC編程的效率。