本篇文章将回答以下几个问题
首先希望你能带着这些问题来看这篇文章,也希望这篇文章能让你很好的解答这些问题。当然,这篇文章的终极目标是希望你能够借鉴spring-jdbc 的思想来解决我们在工作过程中所面临的问题。
如果你想了解,如何使用spring-jdbc,请绕道......
为了实现数据和业务的分离,有人提出了Dao模式。Dao模式是数据处理的一种理想模式,(我认为)它带来了两个方面的好处:1、屏蔽数据访问的差异性;2、业务与数据分离。spring-jdbc 在本质上是一种Dao模式的具体实现。(Dao模式的详细介绍)
接下下我们用一个简单的例子(未具体实现)来简单介绍一下Dao模式(如下图所示)
从上面的UML图可以知道:
public class User { private int id; private String name; private String email; private String phone; public User() { } public int getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } public String getPhone() { return phone; } public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setEmail(String email) { this.email = email; } public void setPhone(String phone) { this.phone = phone; } }
public interface UserDaoInterface { public User getUserInfoByName(String name); public void putUserInfo(User user); public void updateUserInfo(User user); }
public class UserDaoJdbcAccessImpl implements UserDaoInterface { // Jdbc连接数据库等操作,未完成具体实现 private DataSource dataSource; public User getUserInfoByName(String name) { dataSource.getC return new User(); } public void putUserInfo(User user) { } public void updateUserInfo(User user) { } }
public class UserDaoBatisAccessImpl implements UserDaoInterface { // Batis连接数据库等操作,未完成具体实现 public User getUserInfoByName(String name) { return new User(); } public void putUserInfo(User user) { } public void updateUserInfo(User user) { } }
public class UserDaoFacotry { public static UserDaoInterface getUserDao(int which) { switch(which) { case 1: return new UserDaoJdbcAccessImpl(); case 2: return new UserDaoBatisAccessImpl(); default: return null; } } }
public class UserService { public UserDaoInterface getUserDaoOperation() { return UserDaoFacotry.getUserDao(1); } public void getUserInfo() { User user = this.getUserDaoOperation().getUserInfoByName("xiaoming"); } }
但在具体实现DaoImpl时遇到了一个问题,数据库的连接访问会抛出异常,且属于checked exception
public User getUserInfoByName(String name) { try { Connection connection = dataSource.getConnection(); User user = .... return user; } catch (SQLException e) { } finally { connection.close(); } }
这是很尴尬的,因为此时我们不知道是要抛给上层业务还是catch之后进行处理。catch之后进行处理,由于屏蔽异常会让客户端难以排查问题,如果直接抛出去也带来更严重的问题(必须更改接口且不同数据库所抛出的异常不一样),如下所示
public User getUserInfoByName(String name) throw SQLException, NamingException ... { try { Connection connection = dataSource.getConnection(); User user = .... return user; } finally { connection.close(); } }
jdbc 为了解决不同数据库带来的异常差异化,则对异常进行统一转换,并抛出unchecked异常。具体抛出的异常可以在org.springframework.dao中查看
这是很尴尬的,因为此时我们不知道是要抛给上层业务还是catch之后进行处理。catch之后进行处理,由于屏蔽异常会让客户端难以排查问题,如果直接抛出去也带来更严重的问题(必须更改接口且不同数据库所抛出的异常不一样),如下所示
具体异常所代表的含义:
Spring的DAO异常层次
异常 | 何时抛出 |
---|---|
CleanupFailureDataAccessException | 一项操作成功地执行,但在释放数据库资源时发生异常(例如,关闭一个Connection |
DataAccessResourceFailureException | 数据访问资源彻底失败,例如不能连接数据库 |
iMac | 10000 元 |
DataIntegrityViolationException | Insert或Update数据时违反了完整性,例如违反了惟一性限制 |
DataRetrievalFailureException | 某些数据不能被检测到,例如不能通过关键字找到一条记录 |
DeadlockLoserDataAccessException | 当前的操作因为死锁而失败 |
IncorrectUpdateSemanticsDataAccessException | Update时发生某些没有预料到的情况,例如更改超过预期的记录数。当这个异常被抛出时,执行着的事务不会被回滚 |
InvalidDataAccessApiusageException 一个数据访问的JAVA | API没有正确使用,例如必须在执行前编译好的查询编译失败了 |
invalidDataAccessResourceUsageException | 错误使用数据访问资源,例如用错误的SQL语法访问关系型数据库 |
OptimisticLockingFailureException | 乐观锁的失败。这将由ORM工具或用户的DAO实现抛出 |
TypemismatchDataAccessException | Java类型和数据类型不匹配,例如试图把String类型插入到数据库的数值型字段中 |
UncategorizedDataAccessException | 有错误发生,但无法归类到某一更为具体的异常中 |
我们可以将spring-jdbc 看作Dao 模式的一个最佳实践,它只是使用了template模式,实现了最大化的封装,以减少用户使用的复杂性。spring-jdbc 提供了两种模式的封装,一种是Template,一种是操作对象的模式。操作对象的模式只是提供了面向对象的视觉(template 更像面向过程),其底层的实现仍然是采用Template。
接下来我们将会了解Template 的封装过程。
还是延用上述例子,如果这里我们需要根据用户名查询用户的完整信息,将采用下面的方式实现查询
public class UserDaoJdbcAccessImpl implements UserDaoInterface { // Jdbc连接数据库等操作,未完成具体实现 private DataSource dataSource; public User getUserInfoByName(String name) { String sql = "....." + name; Connection connection = null; try { connection = DataSourceUtils.getConnection(dataSource); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(sql); List<User> userList = Lists.newArrayList(); while(resultSet.next()) { User user = new User(); user.setId(resultSet.getInt(1)); user.setName(name); user.setEmail(resultSet.getString(3)); user.setPhone(resultSet.getString(4)); userList.add(user); } connection.close(); connection = null; statement.close(); return userList; } catch (Exception e) { throw new DaoException(e); } finally { if (connection != null) { try { connection.close(); } catch (SQLException e) { log.error("....."); } } } }
当我们只需要完成一个操作的项目时,这种方式还可以接受,但当项目中有大量的DAO需要操作时,难免过程中会出现各种问题,如忘记关闭连接等。
其实我们可以发现整个的数据库的操作实现可以分为四个部分:资源管理(数据库的连接关闭等操作)、sql执行(查询、更新等)、结果集的处理(将sql查询结果转化)、异常处理。
那是不是可以将公共部分抽象成一个模板进行使用呢?现在我们来定义一个Jdbc的一个模板
public class JdbcTemplate { public final Object execute(StatementCallback callback) { Connection connection = null; Statement statement = null; try { connection = getConnetion(); statement = con.createStatement(); Object ret = callback.doWithStatement(callback); return retValue; } catch (SQLException e) { DateAccessException ex = translateSqlException(e); throw ex; } finally { closeStatement(statement); releaseConnection(connection); } } }
Template 定义了关注了操作的所有过程,只需要传递一个callback,就可以帮我们处理各种细节化操作,这些细节化操作包括:获取数据库连接;执行操作;处理异常;资源释放。那我们在使用时就可以简化为
private JdbcTemplate jdbcTemplate; // Jdbc连接数据库等操作,未完成具体实现 private DataSource dataSource; public User getUserInfoByName(String name) { StatementCallback statementCallback = new StatementCallback() { @Override public Object doInStatement(Statement stmt) throws SQLException, DataAccessException { return null; } } return jdbcTemplate.execute(statementCallback); }
实际上,Template 在封装时远比这个复杂,接下来我们就看一下spring-jdbc 是如何对jdbc进行封装的
JdbcTemplate 实现了JdbcOperations接口和继承了JdbcAccessor。
JdbcOperations 定义了数据库的操作,excute、 query、update 等,它是对行为的一种封装。
JdbcAccessor 封装了对资源的操作以及异常的处理,可以看一下源码,比较短。
public abstract class JdbcAccessor implements InitializingBean { /** Logger available to subclasses */ protected final Log logger = LogFactory.getLog(getClass()); private DataSource dataSource; private SQLExceptionTranslator exceptionTranslator; private boolean lazyInit = true; /** * Set the JDBC DataSource to obtain connections from. */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * Return the DataSource used by this template. */ public DataSource getDataSource() { return this.dataSource; } /** * Specify the database product name for the DataSource that this accessor uses. * This allows to initialize a SQLErrorCodeSQLExceptionTranslator without * obtaining a Connection from the DataSource to get the metadata. * @param dbName the database product name that identifies the error codes entry * @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName * @see java.sql.DatabaseMetaData#getDatabaseProductName() */ public void setDatabaseProductName(String dbName) { this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName); } /** * Set the exception translator for this instance. * <p>If no custom translator is provided, a default * {@link SQLErrorCodeSQLExceptionTranslator} is used * which examines the SQLException's vendor-specific error code. * @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator * @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator */ public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) { this.exceptionTranslator = exceptionTranslator; } /** * Return the exception translator for this instance. * <p>Creates a default {@link SQLErrorCodeSQLExceptionTranslator} * for the specified DataSource if none set, or a * {@link SQLStateSQLExceptionTranslator} in case of no DataSource. * @see #getDataSource() */ public synchronized SQLExceptionTranslator getExceptionTranslator() { if (this.exceptionTranslator == null) { DataSource dataSource = getDataSource(); if (dataSource != null) { this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); } else { this.exceptionTranslator = new SQLStateSQLExceptionTranslator(); } } return this.exceptionTranslator; } /** * Set whether to lazily initialize the SQLExceptionTranslator for this accessor, * on first encounter of a SQLException. Default is "true"; can be switched to * "false" for initialization on startup. * <p>Early initialization just applies if {@code afterPropertiesSet()} is called. * @see #getExceptionTranslator() * @see #afterPropertiesSet() */ public void setLazyInit(boolean lazyInit) { this.lazyInit = lazyInit; } /** * Return whether to lazily initialize the SQLExceptionTranslator for this accessor. * @see #getExceptionTranslator() */ public boolean isLazyInit() { return this.lazyInit; } /** * Eagerly initialize the exception translator, if demanded, * creating a default one for the specified DataSource if none set. */ @Override public void afterPropertiesSet() { if (getDataSource() == null) { throw new IllegalArgumentException("Property 'dataSource' is required"); } if (!isLazyInit()) { getExceptionTranslator(); } } }
源码有三个参数:datasource、exceptionTranslator(转换各种数据库方案商的不同的数据库异常)、lazyInit(延时加载:是否在applicationContext 初始化时就进行实例化)
在使用的过程中我们可以看到,只需要提供一个statementCallback,就可以实现对Dao 的各种操作。spring-jdbc 为了满足各种场景的需要,为我们提供了四组不同权限的callback
在使用的过程中我们可以看到,只需要提供一个statementCallback,就可以实现对Dao 的各种操作。spring-jdbc 为了满足各种场景的需要,为我们提供了四组不同权限的callback
callback | 说明 |
---|---|
CallableStatementCallback | 面向存储过程 |
ConnectionCallback | 面向连接的call,权限最大(但一般情况应该避免使用,造成操作不当) |
PreparedStatementCallback | 包含查询询参数的的callback,可以防止sql 注入 |
StatementCallback | 缩小了ConnectionCallback的权限范围,不允许操作数据库的连接 |
我们再看一下JdbcTemplate 的封装
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()); } }
有两个需要注意的地方
Connection con = DataSourceUtils.getConnection(getDataSource());
这里创建连接使用的是DataSourceUtils,而不是datasource.getConnection,这是由于考虑到了事务处理的因素。
if (this.nativeJdbcExtractor != null) { // Extract native JDBC Connection, castable to OracleConnection or the like. conToUse = this.nativeJdbcExtractor.getNativeConnection(con); }
这里并不一定使用的是jdbc的connection,因为jdbc是一种统一化封装,而忽略了各个sql供应商的差异性。有时间我们需要使用某一数据库的某种特性(比如Oracle sql)时,就可以通过对nativeJdbcExtractor来达到目的。
JdbcTemplate 还有几个演生的template,这里都不再详细介绍。
Ok,关于template 的介绍就到此为止(这里更倾向于介绍各种技术的实现原理,而非如何使用)。
对象模式其实只是把Template 中的操作封装成各个对象,而其本质的实现方式仍然是Template
spring-jdbc的封装方式得到了广泛认可,但并不代表它是一个友好的的操作数据库的工具。 从上面的介绍过程中,我们可以感受到jdbc 的封装是面向底层的,所以它对于上层的使用方并不那么友好。jdbc 并未能真正的实现业务和数据的完全分离,对callback的定义仍然会穿插在业务当中,所以在实际的业务应用中,已经很少直接使用jdbc。因此spring 也对很多其它的ORM框架进行了支持,如ibatis,hibernate,JDO等等,这些更高级对用户更加友好。接下我会用一系列文章,对这些框架进行介绍
我们再来回顾一下最前面提出的三个问题:
个人能力有限,有错误之处还请指证.....