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