编程经验点滴----避免在数据库访问函数中使用 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)。

 

你可能感兴趣的:(catch)