统一的数据访问异常体系
问题:DAO模式下,数据访问接口需要抛出异常,如果异常特定于某种实现,那么使用这个接口的代码将会与特定的实现绑定,如果更换实现,接口需要抛出新的异常。
解决:
- 在DAO实现类内部,将数据访问异常封装后以unchecked Exception抛出,因为数据库异常一般也无法恢复。
- DAO实现类提取特定的异常的信息,根据信息抛出不同类型的异常。
DataAccessException
JDBC
问题:
- 繁琐的API,完成一个操作有太多雷同的代码
- API使用容易出错,需要关闭很多资源。
- 异常处理能力不足,一个SQLException,没有子类化,ErrorCode的规范各个数据库厂商来制定
解决:
JdbcTemplate
- 封装所有基于JDBC的数据访问代码
- 对SQLException提供的异常信息进行了统一转译,将JDBC数据异常纳入Spring异常
- 还做了对Spring事务的集成
JdbcTemplate
模板方法模式+CallBack
- 通过模板方法模式固定流程,包括获取资源,执行sql,以及处理异常,关闭资源
- 对于获取到的资源,通过传入CallBack来操作资源,在CallBack中的方法实现需要的逻辑
CallBack公开的资源:
- ConnectionCallback
- StatementCallback
- PreparedStatementCallback
- CallableStatementCallback
public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null) {
// Extract native JDBC Connection, castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
else {
// Create close-suppressing Connection proxy, also preparing returned Statements.
conToUse = createConnectionProxy(con);
}
return action.doInConnection(conToUse);
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
}
finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
execute即为模板方法,固定了执行流程,包括获取资源,捕获异常并转译以及关闭资源,暴露出来的资源由Callback进行使用,CallBack内部的逻辑才是访问数据库的用户逻辑。不同类型的CallBack暴露了不同层级的资源。
DataSourceUtils
获取Connection,绑定到线程
SQLException转译
通过SQLExceptionTranslator将SQLException转译到DataAccessException
数据访问
public void execute(final String sql) throws DataAccessException {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
}
class ExecuteStatementCallback implements StatementCallback<Object>, SqlProvider {
@Override
public Object doInStatement(Statement stmt) throws SQLException {
stmt.execute(sql);
return null;
}
@Override
public String getSql() {
return sql;
}
}
execute(new ExecuteStatementCallback());
}
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
JdbcTemplate中关于数据访问的函数非常的多,但细分之后,基本所有的函数最终都是通过StatementCallback来实现的,如上面的两种,函数内部实现了一个StatementCallback类,然后将SQL语句传入,完成操作,他们的区别在于execute(String)没有返回结果,而query需要处理ResultSet。
那么从最简单的开始
- queryFor*()
queryFor*函数的参数的第一个都是String,代表SQL语句- queryForMap
参数可以加Object...作为查询参数,或者Object[]以及int[]作为查询参数以及参数类型
返回一行结果,Map<String, Object> - queryForObject
参数除了上述的Object[]之外,一般还有一个Class<T>,代表返回的对象类型,或者是RowMapper<T>
返回一行结果,T - queryForList
参数除了上述的Object[]作为查询参数以外,如果还有Class<T>,那么返回对象,否则返回Map
返回List,List<T>或者List<Map<String, Object>>
- queryForMap
对于queryForMap和queryForObject来说,它们也是调用queryForList实现的,但是保证List中只有一个元素。
而对于上面所有的queryFor*函数来说,它们取到的ResultSet都是通过RowMapper处理的,而RowMapper其实也是会被包装成RowMapperResutlSetExtractor,调用上面的第二个函数。
如果加了查询参数,那么不用StatementCallback而是用PreparedStatementCallback,本质也是一样的。
- query()
所以也就是上面的queryFor把参数包装好以后,会调用query函数,有参数的使用PreparedStatementCallback,没参数的使用StatementCallback,而对于ResultSet的处理,有三种回调接口:- RowMapper
处理单行结果,结果由ResultSetExtractor组装成List - RowCallbackHandler
处理单行结果,结果自己处理 - ResutlSetExtractor
自己处理ResultSet
区别:《Spring揭秘》p278
- RowMapper
从上面的函数也可以看到,查询的时候这些ResultSetExtractor最终也是在StatementCallback中使用的。
NamedParameterJdbcTemplate
使用变量名而不是?作为查询参数的占位符