編程經(jīng)驗(yàn)點(diǎn)滴----避免在數(shù)據(jù)庫訪問函數(shù)中使用 try catch
看到很多數(shù)書中的代碼示例,都在數(shù)據(jù)庫訪問函數(shù)中使用 try catch,誤導(dǎo)初學(xué)者,很是痛心。
我們來分析一個常見的函數(shù)(來自國內(nèi)某些大公司的代碼,反面例子,不可仿效),
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 }
這里所說的函數(shù)問題在于,在這樣的調(diào)用情況下會有問題(請發(fā)言者仔細(xì)看看這塊偽代碼):
1) begin database transaction
2) updateData("update user set last_active_time = ...");
3) updateData("insert into ....");
3) ftpSend();
3) sendMail();
4) commit();
updateData() 內(nèi)部就 try catch 或者 commit/rollback ,問題大了!
這里的問題很多:
a) SQL 執(zhí)行出錯后,簡單地輸出到控制臺。沒有把出錯信息,返回或者通過 throw Exception 拋出。結(jié)果很可能是, SQL 運(yùn)行出錯,界面上卻提示“操作成功”。
b) 如果代碼連續(xù)執(zhí)行多個 update/delete,放在一個 transaction 中。SQL 執(zhí)行出錯后,SQLException 被 catch 住,transaction 控制代碼,無法 rollback。
c) 當(dāng)然還有 SQL 注入問題。這里應(yīng)該用 PreparedStatement。
如果要避免代碼“代碼中運(yùn)行出錯,界面上卻提示:操作成功”的問題,則應(yīng)該避免在數(shù)據(jù)庫訪問函數(shù)中使用 try catch。更進(jìn)一步的,在工具類、dao、service 代碼中,都應(yīng)該禁止用 try catch。
那么, try catch 應(yīng)該放在哪里呢?
1) 如果是單機(jī)版程序,出錯信息應(yīng)該提示給用戶,try catch 放在事件響應(yīng)函數(shù)中。當(dāng)然了,如果用 transaction , 也在這里 begin/commit/rollback。
2) 如果是 Web MVC 程序,出錯信息應(yīng)該提示給用戶,try catch 放在 URL 相應(yīng)的事件響應(yīng) java/C# 代碼中。當(dāng)然了,如果用 transaction , 也在這里 begin/commit/rollback。如果是 Java EE 程序,建議在 filter 中,也放一個 try catch,作為全局的 exception 控制,防止萬一有人在 URL 相應(yīng)的事件響應(yīng) java/C# 代碼中漏寫了try catch 。出錯信息也要放在界面上提示給用戶看。
3) 如果是定時(shí)任務(wù),try catch 應(yīng)放在定時(shí)任務(wù)類里,當(dāng)定時(shí)任務(wù)類調(diào)用 dao/service/工具類的時(shí)候,被調(diào)用的函數(shù)都不應(yīng)該有 try catch。出錯信息應(yīng)該記錄在日志中。
4) 如果不用 MVC 的 jsp/asp.net 程序,try catch 怎么處理,就很麻煩。建議不要用這種軟件架構(gòu)。
我覺得正確的代碼應(yīng)該是這樣的:
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 從外面?zhèn)魅耄驗(yàn)閷戇@個 update 的函數(shù)時(shí),還不能確定,實(shí)際業(yè)務(wù)邏輯,是一個 update 函數(shù)就是一個 transaction,還是多個 update/delete 組合在一起,做一個 transaction。
補(bǔ)充:
數(shù)據(jù)庫事務(wù)控制,應(yīng)該從數(shù)據(jù)庫訪問層中獨(dú)立出來,這里是比較正確的控制流程:
用戶點(diǎn)擊 -- 數(shù)據(jù)庫事務(wù)控制層 --- 調(diào)用一個或者多個數(shù)據(jù)訪問層函數(shù) ---- 代碼返回到數(shù)據(jù)庫事務(wù)控制層,決定 commit/rollback。
這樣做的原因在于:無法避免用戶在代碼中連續(xù)調(diào)用多個數(shù)據(jù)訪問層函數(shù),如果在每個數(shù)據(jù)訪問層函數(shù)中,commit/rollback,會造成整個操作有多個數(shù)據(jù)庫事務(wù),以下是錯誤的流程:
用戶點(diǎn)擊 -- 調(diào)用一個或者多個數(shù)據(jù)訪問層函數(shù)(每個函數(shù)中有 commit/rollback)。
可以寫一個這樣類 JdbcTransactionUtils, 其中包含的函數(shù):
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 做成公共函數(shù),因?yàn)槟菢樱渌绦騿T一不小心漏掉了什么,就有問題了。寫公共函數(shù),要做到易用、不易被錯用。
上面的數(shù)據(jù)庫事務(wù)控制函數(shù)可以做到。
然而,這樣還不算完美。畢竟,馬虎的程序員,還是可以在一個 click 中調(diào)用多個數(shù)據(jù)庫事務(wù)控制層,也就是調(diào)用多個 JdbcTransactionUtils.doWithJdbcTransactionDefaultCommit(), 結(jié)果如下:
用戶點(diǎn)擊 -- 數(shù)據(jù)庫事務(wù)控制層函數(shù)1 --- 調(diào)用一個或者多個數(shù)據(jù)訪問層函數(shù) ---- 代碼返回到數(shù)據(jù)庫事務(wù)控制層,決定 commit/rollback -- 數(shù)據(jù)庫事務(wù)控制層函數(shù)2 --- 調(diào)用一個或者多個數(shù)據(jù)訪問層函數(shù) ---- 代碼返回到數(shù)據(jù)庫事務(wù)控制層,決定 commit/rollback。
還是不好。
實(shí)際上,我們期望的是,每次用戶點(diǎn)擊,后臺都應(yīng)該是一個數(shù)據(jù)庫 transaction,因此,我的意思是,數(shù)據(jù)庫事務(wù)控制代碼,要和 web 層的后臺處理代碼(比如 struts 的 action , asp.net 頁面對應(yīng)的 .cs 文件),合并掉,并在此處理 try catch。至于其他被調(diào)用的函數(shù),比如數(shù)據(jù)庫訪問函數(shù),比如工具類,都不要 try catch。畢竟,數(shù)據(jù)庫訪問函數(shù),比如工具類,都可能被多個地方的代碼調(diào)用,如果在里面寫 try catch, 如何寫 try catch 達(dá)到所有調(diào)用的模塊都滿意,是很難做到的。
最后我認(rèn)為合理的流程如下:
用戶點(diǎn)擊 -- 用戶點(diǎn)擊處理程序(struts action/asp.net 頁面.cs),包含 try catch,包含數(shù)據(jù)庫事務(wù)控制 --- 調(diào)用一個或者多個數(shù)據(jù)訪問層函數(shù)(無 try catch) --- 調(diào)用一個或者多個工具類函數(shù)(無 try catch)。

浙公網(wǎng)安備 33010602011771號