spring-jdbc 的实现原理

前言

本篇文章将回答如下几个问题html

  1. spring-jdbc 的出现是为了解决什么问题
  2. spring-jdbc 如何解决的这些问题
  3. 它的这种技术有何缺陷

首先但愿你能带着这些问题来看这篇文章,也但愿这篇文章能让你很好的解答这些问题。固然,这篇文章的终极目标是但愿你可以借鉴spring-jdbc 的思想来解决咱们在工做过程当中所面临的问题。
若是你想了解,如何使用spring-jdbc,请绕道......java

Dao 模式

为了实现数据和业务的分离,有人提出了Dao模式。Dao模式是数据处理的一种理想模式,(我认为)它带来了两个方面的好处:一、屏蔽数据访问的差别性;二、业务与数据分离。spring-jdbc 在本质上是一种Dao模式的具体实现。(Dao模式的详细介绍
接下下咱们用一个简单的例子(未具体实现)来简单介绍一下Dao模式(以下图所示)spring


从上面的UML图能够知道:

  • 首先定义了一个User的操做接口UserDao,它定义了了获取用户信息、添加用户信息、更改用户信息的行为;
  • 具体行为的由其实现类来实现,咱们这里举了两个例子:Batis 实现和Jdbc实现(固然也能够缓存实现或file实现等),它实现具体获取或修改数据的行为;UserDaoFactory 生成具体的实现UserDao实现类(请参考下面代码)。
  • 因此当咱们在Service层(UserService)访问数据时,只 须要使用UserDaoFactory 生成一个具体的UserDao实现类就能够了,这样业务层就能够彻底操做数据操做的具体实现( 参考下面UserService的具体实现)
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 exceptionsql

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以后进行处理,因为屏蔽异常会让客户端难以排查问题,若是直接抛出去也带来更严重的问题(必须更改接口且不一样数据库所抛出的异常不同),以下所示bash

具体异常所表明的含义:
Spring的DAO异常层次 oracle

异常 什么时候抛出
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

咱们能够将spring-jdbc 看做Dao 模式的一个最佳实践,它只是使用了template模式,实现了最大化的封装,以减小用户使用的复杂性。spring-jdbc 提供了两种模式的封装,一种是Template,一种是操做对象的模式。操做对象的模式只是提供了面向对象的视觉(template 更像面向过程),其底层的实现仍然是采用Template。
接下来咱们将会了解Template 的封装过程。app

2.1 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 的介绍就到此为止(这里更倾向于介绍各类技术的实现原理,而非如何使用)。

2.2 对象模式

对象模式其实只是把Template 中的操做封装成各个对象,而其本质的实现方式仍然是Template

3、缺陷

spring-jdbc的封装方式获得了普遍承认,但并不表明它是一个友好的的操做数据库的工具。 从上面的介绍过程当中,咱们能够感觉到jdbc 的封装是面向底层的,因此它对于上层的使用方并不那么友好。jdbc 并未能真正的实现业务和数据的彻底分离,对callback的定义仍然会穿插在业务当中,因此在实际的业务应用中,已经不多直接使用jdbc。所以spring 也对不少其它的ORM框架进行了支持,如ibatis,hibernate,JDO等等,这些更高级对用户更加友好。接下我会用一系列文章,对这些框架进行介绍

4、总结

咱们再来回顾一下最前面提出的三个问题:

  1. spring-jdbc 是为了解决数据和业务分离的问题,使客户端可以更专一于业务层面,而没必要关注数据库资源的链接释放及异常处理等逻辑。
  2. spring-jdbc 采用dao模式实现了业务和数据的分离;使用模板模式,实现了逻辑的封装
  3. spring-jdbc 属于面向低层的实现,对用户不太友好。

我的能力有限,有错误之处还请指证.....

相关文章
相关标签/搜索