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