看到很多數書中的代碼示例,都在數據庫訪問函數中使用 try catch,誤導初學者,很是痛心。
我們來分析一個常見的函數(來自國內某些大公司的代碼,反面例子,不可仿效),
1 public int updateData(String sql) { 2 int resultRow = 0; 3 try{ 4 Connection con = ... 5 statement = con.createStatement(); 6 resultRow = statement.executeUpdate(sql); 7 ... 8 } catch (SQLException e) { 9 e.printStackTrace(); 10 } 11 return resultRow; 12 }
這里所說的函數問題在於,在這樣的調用情況下會有問題(請發言者仔細看看這塊偽代碼):
1) begin database transaction
2) updateData("update user set last_active_time = ...");
3) updateData("insert into ....");
3) ftpSend();
3) sendMail();
4) commit();
updateData() 內部就 try catch 或者 commit/rollback ,問題大了!
這里的問題很多:
a) SQL 執行出錯后,簡單地輸出到控制台。沒有把出錯信息,返回或者通過 throw Exception 拋出。結果很可能是, SQL 運行出錯,界面上卻提示“操作成功”。
b) 如果代碼連續執行多個 update/delete,放在一個 transaction 中。SQL 執行出錯后,SQLException 被 catch 住,transaction 控制代碼,無法 rollback。
c) 當然還有 SQL 注入問題。這里應該用 PreparedStatement。
如果要避免代碼“代碼中運行出錯,界面上卻提示:操作成功”的問題,則應該避免在數據庫訪問函數中使用 try catch。更進一步的,在工具類、dao、service 代碼中,都應該禁止用 try catch。
那么, try catch 應該放在哪里呢?
1) 如果是單機版程序,出錯信息應該提示給用戶,try catch 放在事件響應函數中。當然了,如果用 transaction , 也在這里 begin/commit/rollback。
2) 如果是 Web MVC 程序,出錯信息應該提示給用戶,try catch 放在 URL 相應的事件響應 java/C# 代碼中。當然了,如果用 transaction , 也在這里 begin/commit/rollback。如果是 Java EE 程序,建議在 filter 中,也放一個 try catch,作為全局的 exception 控制,防止萬一有人在 URL 相應的事件響應 java/C# 代碼中漏寫了try catch 。出錯信息也要放在界面上提示給用戶看。
3) 如果是定時任務,try catch 應放在定時任務類里,當定時任務類調用 dao/service/工具類的時候,被調用的函數都不應該有 try catch。出錯信息應該記錄在日志中。
4) 如果不用 MVC 的 jsp/asp.net 程序,try catch 怎么處理,就很麻煩。建議不要用這種軟件架構。
我覺得正確的代碼應該是這樣的:
import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; import org.apache.commons.dbutils.DbUtils; public class MyJdbcUitls { public int updateData(Connection con, String sql, List<Object> paramValueList) throws SQLException { // int resultRow = 0; try{ // Connection con = ... // statement = con.createStatement(); // resultRow = statement.executeUpdate(sql); // ... } catch (SQLException e) { // e.printStackTrace(); } // return resultRow; }} PreparedStatement ps = null; try { ps = con.prepareStatement(sql); if (paramValueList != null) { for (int i = 0; i < paramValueList.size(); i++) { setOneParameter(i, ps, paramValueList.get(i)); } } int count = ps.executeUpdate(); return count; } finally { DbUtils.closeQuietly(ps); } } }
注意:
之所以要把 connection 從外面傳入,因為寫這個 update 的函數時,還不能確定,實際業務邏輯,是一個 update 函數就是一個 transaction,還是多個 update/delete 組合在一起,做一個 transaction。
補充:
數據庫事務控制,應該從數據庫訪問層中獨立出來,這里是比較正確的控制流程:
用戶點擊 -- 數據庫事務控制層 --- 調用一個或者多個數據訪問層函數 ---- 代碼返回到數據庫事務控制層,決定 commit/rollback。
這樣做的原因在於:無法避免用戶在代碼中連續調用多個數據訪問層函數,如果在每個數據訪問層函數中,commit/rollback,會造成整個操作有多個數據庫事務,以下是錯誤的流程:
用戶點擊 -- 調用一個或者多個數據訪問層函數(每個函數中有 commit/rollback)。
可以寫一個這樣類 JdbcTransactionUtils, 其中包含的函數:
public static void doWithJdbcTransactionDefaultCommit(SqlRunnable run, Connection con) { doWithJdbcTransactionNoCommitRollback(run, con); try { con.commit(); } catch (Exception e) { Log log = LogFactory.getLog(JdbcTransactionUtils.class); log.error(e.getMessage(), e); try { con.rollback(); } catch (Exception err) { log.error(err.getMessage(), err); } throw new NestableRuntimeException(e.getMessage(), e); } }
要避免把 commit/rollback 做成公共函數,因為那樣,其他程序員一不小心漏掉了什么,就有問題了。寫公共函數,要做到易用、不易被錯用。
上面的數據庫事務控制函數可以做到。
然而,這樣還不算完美。畢竟,馬虎的程序員,還是可以在一個 click 中調用多個數據庫事務控制層,也就是調用多個 JdbcTransactionUtils.doWithJdbcTransactionDefaultCommit(), 結果如下:
用戶點擊 -- 數據庫事務控制層函數1 --- 調用一個或者多個數據訪問層函數 ---- 代碼返回到數據庫事務控制層,決定 commit/rollback -- 數據庫事務控制層函數2 --- 調用一個或者多個數據訪問層函數 ---- 代碼返回到數據庫事務控制層,決定 commit/rollback。
還是不好。
實際上,我們期望的是,每次用戶點擊,后台都應該是一個數據庫 transaction,因此,我的意思是,數據庫事務控制代碼,要和 web 層的后台處理代碼(比如 struts 的 action , asp.net 頁面對應的 .cs 文件),合並掉,並在此處理 try catch。至於其他被調用的函數,比如數據庫訪問函數,比如工具類,都不要 try catch。畢竟,數據庫訪問函數,比如工具類,都可能被多個地方的代碼調用,如果在里面寫 try catch, 如何寫 try catch 達到所有調用的模塊都滿意,是很難做到的。
最后我認為合理的流程如下:
用戶點擊 -- 用戶點擊處理程序(struts action/asp.net 頁面.cs),包含 try catch,包含數據庫事務控制 --- 調用一個或者多個數據訪問層函數(無 try catch) --- 調用一個或者多個工具類函數(無 try catch)。