程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 消除JDBC的瓶頸

消除JDBC的瓶頸

編輯:關於JAVA

摘要

大部分的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、執行語句\r

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

還有,你必須考慮那些不斷產生的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 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()只是將參數對象數組中的每個對象放入到預處理語句中的相應位置。例如:

...//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類型。

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

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

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

查詢例子

你經常都需要利用一個用戶的信息表由數據庫中得到一個用戶的對象,假設我們使用以下的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)

如果我們沒有使用這篇文章講述的架構,我們將需要一個頗大的方法來處理由數據庫中接收用戶信息並且形成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()]);

最後,我們將寫一個方法來執行查詢並且返回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語句,並且可以重新使用以前方法的用戶處理器。由於返回行的元數據並不依賴查詢,所以我們可以重新使用結果處理器。

更新的架構

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

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);}}

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

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

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

更新的例子

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

首先,構造SQL語句:

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

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

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);}}}

最後就寫編寫執行更新的方法:

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就執行了一個更新的操作。

事務\r

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

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

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

為了令事務工作,我們必須保持狀態,特別是連接的狀態。直到現在,SQLProcessor還是一個無狀態的類。它缺乏成員變量。為了重用SQLProcessor,我們創建了一個事務封裝類,它接收一個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。其後的每個執行都是使用同一個連接(傳送到SQLProcessor的handleUpdate()方法中),因此事務保持完整。

只有當我們的持久性對象或者方法調用commitTransaction()時,事務才被提交,並且關閉連接。如果在執行期間發生了異常,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();}

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

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

問題\r

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

自定義連接

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

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

ResultProcessor返回類型

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

SQLProcessor初始管理\r

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

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

public SQLTransaction startTransaction() {

return new SQLTransaction(this);

}

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

數據庫異常

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

省心的JDBC programming

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved