top
Loading...
[SQL]:如何簡化JDBC代碼

問題的提出

在一個應用程序中,處理JDBC的操作是一個重復率較高的工作。當你在一個JDBC數據源上執行SQL查詢時,你通常需要執行下面幾個步驟:

1.生成SQL語句

2.獲得連接

3.獲得一個PreparedStatement對象

4.在PreparedStatement對象中設定需要送入到數據庫的值

5.執行SQL語句

6.處理查詢結果

除此之外,你還需要處理SQLException異常。如果上面列出的這些步驟分散在程序的各個部分的話,程序中需要多個try/catch塊來處理異常。

如果我們仔細看看上面列出的步驟,就會發現在執行不同的SQL語句時,上面這些步驟中涉及到的程序代碼變化不會很大:我們使用同樣的方法獲得數據庫連接和PreperedStatement對象;使用setXXX方法來設定PreperedStatement對象中的值;處理SQL查詢結果的過程也是基本不變的。在這篇文章中,我們通過定義三個JDBC模型,去除了上述六個步驟中的三個步驟,這樣使得整個過程更加簡單,而且具有更好的通用性。

查詢模型

我們定義了一個名叫SQLProcessor的類,在該類中定義了一個executeQuery()方法來執行SQL語句,我們在實現這個方法的時候盡量保持代碼的簡潔性,并且傳遞盡可能少的參數給該方法。下面是該方法的定義:

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

我們知道在執行SQL語句的JDBC過程中,變化的因素有三個:SQL語句,PreparedStatement對象和如何解釋和處理查詢結果。在上面的方法定義中,sql中保存的就是SQL語句;pStmntValues對象數組保存的是需要放入preparedStatement對象中的值;processor參數是一個能夠處理查詢結果的對象,于是我們把JDBC程序涉及到的對象分成了三個部分。下面讓我們來看一下executeQuery()和與它相關的一些方法的實現:

public class SQLProcessor {  public Object[] executeQuery(String sql, Object[] pStmntValues,                               ResultProcessor processor) {    //獲得連接    Connection conn = ConnectionManager.getConnection();    //將SQL語句的執行重定向到handlQuery()方法    Object[] results = handleQuery(sql, pStmntValues, processor, conn);    //關閉連接    closeConn(conn);    //返回結果    return results;  }  protected Object[] handleQuery(String sql, Object[] pStmntValues,                     ResultProcessor processor, Connection conn) {    //獲得一個preparedStatement對象    PreparedStatement stmnt = null;    try {      //獲得preparedStatement      stmnt = conn.prepareStatement(sql);      //向preparedStatement中送入值      if(pStmntValues != null) {        PreparedStatementFactory.buildStatement(stmnt, pStmntValues);      }      //執行SQL語句      ResultSet rs = stmnt.executeQuery();      //獲得查詢結果      Object[] results = processor.process(rs);      //關閉preparedStatement對象      closeStmnt(stmnt);      //返回結果      return results;      //處理異常      } catch(SQLException e) {        String message = "無法執行查詢語句 " + sql;        //關閉所有資源        closeConn(conn);        closeStmnt(stmnt);        //拋出DatabaseQueryException        throw new DatabaseQueryException(message);      }    }  }...}

程序中有兩個方法需要說明:PreparedStatementFactory.buildStatement()和processor.process()。buildStatement()方法把在pStmntValues對象數組中的所有對象送到prepareStatement對象中的相應位置。例如:

...//取出對象數組中的每個對象的值,//在preparedStatement對象中的相應位置設定對應的值for(int i = 0; i < values.length; i++) {  //如果對象的值為空, 設定SQL空值  if(value instanceof NullSQLType) {    stmnt.setNull(i + 1, ((NullSQLType) value).getFieldType());  } else {    stmnt.setObject(i + 1, value);  }}

因為stmnt.setOject(int index, Object value)方法不能接受一個空對象作為參數。為了使程序能夠處理空值,我們使用了自己設計的NullSQLType類。當一個NullSQLType對象被初始化的時候,將會保存數據庫表中相應列的SQL類型。在上面的例子中我們可以看到,NULLSQLType對象的屬性中保存了一個SQL NULL實際對應的SQL類型。我們用NULLSQLType對象的getFieldType()方法來向preparedStatement對象填入空值。

下面讓我們來看一看processor.process()方法。Processor類實現了ResultProcessor接口,該接口是用來處理SQL查詢結果的,它只有一個方法process(),該方法返回了處理SQL查詢結果后生成的對象數組。

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

process()的典型實現方法是遍歷查詢后返回的ResultSet對象,將保存在ResultSet對象中的值轉化為相應的對象放入對象數組。下面我們通過一個例子來說明如何使用這些類和接口。例如當我們需要從數據庫的一張用戶信息表中取出用戶信息,表名稱為User:

列名 數據類型
ID NUMBER
UserName VARCHAR2
Email VARCHAR2

我們需要在程序中定義一個類User來映射上面的表:

public User(int id, String userName, String email)

如果我們使用常規方法來讀取User表中的數據,我們需要一個方法來從數據庫表中讀取數據,然后將數據送入User對象中。而且一旦查詢語句發生變化,我們需要修改大量的代碼。讓我們看一看采用本文描述的解決方案情況會如何。

首先構造一個SQL語句。

private static final String SQL_GET_USER = "SELECT * FROM USERS WHERE ID = ?";

然后創建一個ResultProcessor接口的實例類,通過它我們可以從查詢結果中獲得一個User對象。

public class UserResultProcessor implements ResultProcessor {  // 列名稱定義(省略)  ...  public Object[] process(ResultSet rs) throws SQLException {    // 使用List對象來保存所有返回的User對象    List users = new ArrayList();    User user = null;    // 如果查詢結果有效,處理查詢結果    while(rs.next()) {      user = new User(rs.getInt(COLUMN_ID), rs.getString(COLUMN_USERNAME),                      rs.getString(COLUMN_EMAIL));      users.add(user);    }    return users.toArray(new User[users.size()]);

最后,將執行SQL查詢和返回User對象的指令放入getUser()方法中。

public User getUser(int userId) {  // 生成一個SQLProcessor對象并執行查詢  SQLProcessor processor = new SQLProcessor();  Object[] users = processor.executeQuery(SQL_GET_USER_BY_ID,                                          new Object[] {new Integer(userId)},                                          new UserResultProcessor());  // 返回查詢到的第一個User對象  return (User) users[0];}

這就是我們需要做的全部工作:只需要實現一個processor類和一個getUser()方法。與傳統的JDBC程序相比,在本文描述的模型中,我們不需要處理數據庫連接操作,生成prepareStatement對象和異常處理部分的代碼。如果需要在同一張表中根據用戶名查用戶ID,我們只需要在代碼中申明新的查詢語句,然后重用UserResultProcessor類中的大部分代碼。

更新模型

如果SQL語句中涉及到更新,情況又會怎樣呢?我們可以用類似于設計查詢模型的方法來設計更新模型,我們需要向SQLProcessor類中增加一些新的方法。這些方法同executeQuery()和handleQuery()方法有相似之處,只是我們需要改變一下處理ResultSet對象的代碼,并且把更新的行數作為方法的返回值。

public void executeUpdate(String sql, Object[] pStmntValues,                          UpdateProcessor processor) {  // 獲得數據庫連接  Connection conn = ConnectionManager.getConnection();  // 執行SQL語句  handleUpdate(sql, pStmntValues, processor, conn);  // 關閉連接  closeConn(conn);}protected void handleUpdate(String sql, Object[] pStmntValues,                            UpdateProcessor processor, Connection conn) {  PreparedStatement stmnt = null;  try {  stmnt = conn.prepareStatement(sql);  // 向prepareStatement對象中送入值  if(pStmntValues != null) {    PreparedStatementFactory.buildStatement(stmnt, pStmntValues);  }  // 執行更新語句  int rows = stmnt.executeUpdate();  // 統計有多少行數據被更新  processor.process(rows);  closeStmnt(stmnt);  // 異常處理  } catch(SQLException e) {    String message = "無法執行查詢語句 " + sql;    closeConn(conn);    closeStmnt(stmnt);    throw new DatabaseUpdateException(message);  }}

上面的兩個方法和處理查詢的方法不同之處在于他們如何處理返回值。由于更新語句只需要返回被更新了的行數,所以我們不需要處理SQL操作返回的結果。實際上有些情況下連被更新了的行數都不需要返回,我們這樣做的原因是在某些情況下需要確認更新操作已經完成。

我們設計了UpdateProcessor接口來處理Update操作返回的更新行數。

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

例如在程序中需要保證更新操作,更新表中的至少一條記錄。在UpdateProcessor接口的實現類中就可以加入對修改行數的檢測,當沒有記錄被更新時,processor()方法可以拋出自定義的異常;也可以將更新的行數記錄到日志文件中;或者激發一個自定義的更新事件。總而言之,你可以在其中做任何事。

下面是一個使用更新模型的例子:

首先生成SQL語句

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

實現UpdateProcessor接口。在Processor()方法中,檢查Update操作是否更新了數據。如果沒有,拋出IllegalStateException異常。

public class MandatoryUpdateProcessor implements UpdateProcessor {  public void process(int rows) {    if(rows < 1) {      String message = "更新操作沒有更新數據庫表中的數據。";      throw new IllegalStateException(message);    }  }}

最后在updateUser()方法中執行Update操作并處理結果。

public static void updateUser(User user) {  SQLProcessor sqlProcessor = new SQLProcessor();  sqlProcessor.executeUpdate(SQL_UPDATE_USER,                             new Object[] {user.getUserName(),                                           user.getEmail(),                                           new Integer(user.getId())},                             new MandatoryUpdateProcessor());}

事務模型

在數據庫中,事務和獨立的SQL語句的區別在于事務在生命期內使用一個數據庫連接,并且AutoCommit屬性必須被設為False。因此我們需要指定事務何時開始,何時結束,并且在事務結束時提交事務。我們可以重用SQLProcessor中的大部分代碼來處理事務。也許在最開始讀者會問為什么要把執行更新和處理更新的工作放在 executeUpdate()和handleUpdate()兩個函數中完成--實際上它們是可以被合并到同一個函數中的。這樣做的原因是把處理數據庫連接的代碼和處理SQL操作的代碼分離開來。對于需要在多個SQL操作間共用數據庫連接的事務模型來說,這種方案便于編碼。

在事務中,我們需要保存事務的狀態,特別是數據庫連接的狀態。前面的SQLProcessor中沒有保存狀態的屬性,為了保證對SQLProcessor類的重用,我們設計了一個包裝類,該類包裝了SQLProcessor類,并且可以維護事務在生命周期內的狀態。

public class SQLTransaction {  private SQLProcessor sqlProcessor;  private Connection conn;  // 缺省構造方法,該方法初始化數據庫連接,并將AutoCommit設定為False  ...  public void executeUpdate(String sql, Object[] pStmntValues,                            UpdateProcessor processor) {    // 獲得結果。如果更新操作失敗,回滾到事務起點并拋出異常    try {       sqlProcessor.handleUpdate(sql, pStmntValues, processor, conn);    } catch(DatabaseUpdateException e) {       rollbackTransaction();       throw e;    }   }  public void commitTransaction() {    // 事務結束,提交更新并回收資源    try {      conn.commit();      sqlProcessor.closeConn(conn);    // 如果發生異常,回滾到事務起點并回收資源    } catch(Exception e) {      rollbackTransaction();      throw new DatabaseUpdateException("無法提交當前事務");    }  }  private void rollbackTransaction() {    // 回滾到事務起點并回收資源    try {      conn.rollback();      conn.setAutoCommit(true);      sqlProcessor.closeConn(conn);    // 如果在回滾過程中發生異常,忽略該異常    } catch(SQLException e) {      sqlProcessor.closeConn(conn);    }  }}

SQLTransaction中出現了一些新方法,這些方法主要是用來處理數據庫連接和進行事務管理的。當一個事務開始時,SQLTransaction對象獲得一個新的數據庫連接,并將連接的AutoCommit設定為False,隨后的所有SQL語句都是用同一個連接。

只有當commitTransaction()被調用時,事務才會被提交。如果執行SQL語句的過程中發生了異常,程序會自動發出一個回滾申請,以恢復程序對數據庫所作的改變。對于開發人員來說,不需要擔心在出現異常后處理回滾或關閉連接的工作。下面是一個使用事務模型的例子。

public static void updateUsers(User[] users) { // 開始事務  SQLTransaction trans = sqlProcessor.startTransaction();  // 更新數據  User 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());  }  // 提交事務  trans.commitTransaction();}

在例子中我們只使用了更新語句(在大多數情況下事務都是由更新操作構成的),查詢語句的實現方法和更新語句類似。

問題

在實際使用上面提到的這些模型時,我遇到了一些問題,下面是這些問題的小結,希望對大家有所幫助。

自定義數據庫連接

在事務處理的時候,有可能發生在多個事務并存的情況下,它們使用的數據庫連接不同的情況。ConnectionManager需要知道它應該從數據庫連接池中取出哪一個連接。你可以簡單修改一下模型來滿足上面的要求。例如在executeQuery()和executeUpdate()方法中,你可以把數據庫連接作為參數,然后將它們傳送給ConnectionManager對象。請記住所有的連接管理都應該放在executeXXX()方法中。另外一種解決方案,也是一種更面向對象化的解決方案,是將一個連接工廠作為參數傳遞給SQLProcessor的構造函數。對于不同的連接工廠類型,我們需要不同的SQLProcessor對象。

ResultProcessor類的返回值:對象數組還是List?

為什么ResultProcessor接口中的process()方法返回的是對象數組呢?怎么不使用List類呢?這是由于在很多實際的應用中,SQL查詢在大多數情況下值返回一行數據,在這種情況下,使用List對象會有些多余了。但是如果你確信SQL查詢將返回多行結果,你可以使用List對象。

數據庫操作異常

我們可以用多個自定義的數據庫操作異常類來替代運行時發生的SQLException異常。最好在這些自定義的異常類時繼承RuntimeException類,這樣可以將這些異常進行集中處理。也許你會認為因該將異常處理放在發生異常的地方。但是我們設計這個的模型的目的之一是在JDBC應用程序開發中去掉或弱化異常處理的部分,只有使用RuntimeException我們才可能達到這個目的。

作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗