看到很多数书中的代码示例,都在数据库访问函数中使用 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)。