編程經驗點滴----避免在數據庫訪問函數中使用 try catch


看到很多數書中的代碼示例,都在數據庫訪問函數中使用 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)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM