编程经验点滴----避免在数据库访问函数中使用 try catch

2012-05-04 14:22

 

看到很多数书中的代码示例,都在数据库访问函数中使用 try catch,误导初学者,很是痛心。 如果要避免代码“代码中运行出错,界面上却提示:操作成功”的问题,则应该避免在数据库访问函数中使用 try catch。

 

看到很多数书中的代码示例,都在数据库访问函数中使用 try catch,误导初学者,很是痛心。

国内常见的技术网站,比如 cnblogs, oschina, csdn 等,也是如此。

我们来分析一个常见的函数(来自国内某些大公司的代码,反面例子,不可仿效),java 代码


public int updateData(String sql)     {
	int resultRow = 0;         
	try{
		 Connection con = ...
		 statement = con.createStatement();             
		 resultRow = statement.executeUpdate(sql);
		 ...                      
	} catch (SQLException e) {
		 e.printStackTrace();         
	}                   
	return resultRow;     
} 
		

这里所说的函数问题在于,在这样的调用情况下会有问题(请发言者仔细看看这块伪代码):


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)。
		

 

 

欢迎转载,转载请注明出处: https://www.zheguisoft.com/staff_blogs/jacklondon_chen/2012, 及 https://www.cnblogs.com/jacklondon/archive/2012/05/04/2482659.html