第14章 JDBC API的最佳实践

本章内容

  • 基于Template的JDBC使用方式
  • 基于操作对象的JDBC使用方式

Spring提供了两种使用JDBC API的最佳实践,一种是以JdbcTemplate为核心的基于Template的JDBC使用方式,另一种则是在JdbcTemplate基础之上构建的基于操作对象的JDBC使用方式。

下面让我们先从基于Template的JDBC使用方式开始看起。

14.1 基于Template的JDBC使用方式

基于 Template 的 JDBC 使用方式的最初设想和原型,需要追溯到 Rod Johnson 在2003年出版的 Expert One on One J2EE Design and Development 一书,在该书的“Practical Data Access(数据访问实践)”一章中,Rod 针对 JDBC 使用中的一些问题提出了一套改进的实践原型,并最终将该原型完善后在 Spring 框架中发布。

下面是我们对这段旅程的再次回顾。

JDBC的尴尬

JDBC作为Java平台访问关系数据库的标准API,其成功是有目共睹的。几乎所有Java平台的数据访问,都直接地或者间接地使用了JDBC,它是整个Java平台面向关系数据库进行数据访问的基石。

作为一个标准,无疑 JDBC 是成功的。但是要说 JDBC 在实际的使用过程中也是受人欢迎的,则不尽然了。JDBC 标准主要面向较为底层的数据库操作,所以在设计过程中,比较贴近底层以提供尽可能多的功能特色。 从这个角度来说,JDBC API 的设计无可厚非。可是,过于贴近底层的 API 设计,对于开发人员的使用来说就不一定是好事了。即使执行简单的查询或者更新,开发人员也要按照 API 的规矩写上一大堆雷同的代码。 如果不能合理封装使用 JDBC API 的代码,在项目中使用 JDBC 访问数据所出现的问题估计会让人抓狂。

对于通常的项目开发来说,如果层次划分明确,数据访问逻辑一般在相应的DAO中实现。根据功能模块的划分,可能每个开发人员都会分得或多或少的实现相应DAO的任务。假设开发人员A在分得了DAO实现任务之后开始开发,他可能开发了如下方代码清单所示的DAO实现代码。

 
    public class DAOWithA implements IDAO {
        private final Log logger = LogFactory.getLog(DAOWithA.class);
 
        public int updateSomething(String sql) {
            int count;
            Connection con = null;
            Statement stmt = null;
            try {
                con = getDataSource().getConnection();
                stmt = con.createStatement();
                count = stmt.executeUpdate(sql);
                stmt.close();
                stmt = null;
            } catch (SQLException e) {
                throw new DaoException(e);
            } finally {
                if (stmt != null) {
                    try {
                        stmt.close();
                    } catch (SQLException ex) {
                        logger.warn("failed to close statement:" + ex);
                    }
                }
                if (con != null) {
                    try {
                        con.close();
                    } catch (SQLException e) {
                        logger.warn("failed to close Connection:+" + ex);
                    }
 
                }
            }
            return count;
        }
    }
 
 

而B所负贵的DAO实现中,可能也有类似的更新操作。无疑,B也要像A这样,在他的DAO中写下同样的一堆JDBC代码,类似的情况可以扩”展到C、D等开发人员。如果每个开发人员都能够严格地按照JDBC编程规范进行编码还好,起码能够保证该避免的问题都能够避免掉,虽然每次都是重复基本相同的一堆代码。

有一个事实是,一个团队中的开发人员是有差别的,可能有的开发人员有好的编程习惯并按照规范保证DAO实现没有问题,但无法保证其他开发人员也能够如此。

当你看到应用程序中那几十甚至上百的使用JDBC实现的各个DAO实现类的时候,我可以保证你将能够发现很多问题:

  • Statement使用完后没关闭,而想着让Connection关闭的时候一并关闭,这倒是省事儿了,可是并非所有的驱动程序都有这样的行为;

  • 创建了多个ResultSet或者Statement,最后却只清理了最外层的,而忽视了里层的;

  • 好不容易忙活完try子句里面的数据访问逻辑,却完全忽视了使用的Connection还没有释放;

这时你会说,“JDBC用起来可真烦人!” 嗯,不得不承认,确实如此。

不过,除了API的使用,JDBC规范在制定 数据访问异常处理 的时候也没能够“将革命进行到底”。

  • 在最初的JDBC规范中,通过一个SQLException类型来包括一切数据访问异常情况,将SQLException声明为checkedException合适与否,我们之前已经提过,显然这是一个需要改进的地方,而最新版本的JDBC规范也就这一点进行了改进,不过这是后话;
  • 除此之外, SQLException 没有采用将具体的异常情况子类化,以进一步抽象不同的数据访问异常情况,而是采用 ErorCode 的方式来区分数据访问过程中所出现的不同异常情况 。其实,这也没什么,只要能够区分具体错误情况就行,但是,JDBC 规范却把 ErrorCode 的规范制定留给了各个数据库提供商(当然,可能并非有意,或者有什么难言之隐)。这导致不同提供商提供的数据库对应不同的 ErrorCode。进而应用程序在捕获 SQLException 之后,还要先看看当前使用的是什么数据库,然后再从 SQLException 中通过 getErrorCode()取得相应 ErrorCode,并与数据库提供商提供的 ErrorCode 列表进行对比,最终才能搞清楚到底哪里出了问题。如果当初 JDBC 规范能够明确规定“ErrorCode == 1代表数据库连接不上,ErrorCode=2代表要访问的表不存在…”现在处理 SQLException 的时候也不会如此痛苦了。

注意:Java平台从1.1版本开始引入JDBC,迄今已经经历了几个大的版本变动,JDBC标准的制定也吸取了Spring框架的JDBC抽象层的部分经验,比如对数据访问异常的层次体系的处理方面。我们可以在最新的JDBC规范中看到这些改进后的亮点。

针对JDBC API在使用中容易出错,使用烦琐的问题,以及SQLException对数据访问异常处理能力不足尚待改进等情况,Spring框架提出了一套针对JDBC使用方面的框架类,以改进JDBC API使用过程中的种种不便甚至不合理之处,帮助我们进一步提高开发过程中使用JDBC进行数据访问的开发效率

JdbcTemplate的诞生

为了解决JDBC API在实际使用中的种种尴尬局面,Spring框架提出了org.springframework.jdbc.core.JdbcTemplate作为数据访问的Helper类。

JdbcTemplate 是整个 Spring 数据抽象层提供的所有 JDBC API 最佳实践的基础,框架内其他更加方便的 Helper 类以及更高层次的抽象,全部构建于 JdbcTemplate 之上。 抓住 JdbcTemplate,就抓住了 Spring 框架 JDBCAPI 最佳实践的核心。

概括地说,JdbcTemplate主要关注如下两个事情。

  • 封装所有基于 JDBC 的数据访问代码,以统一的格式和规范来使用 JDBC API。 所有基于 JDBC 的数据访问需求现在全部通过 JdbcTemplate 进行,从而避免了让烦琐易错的基于 JDBC API 的数据访问代码散落于系统各处。
  • 对 SQLException 所提供的异常信息在框架内进行统一转译, 将基于 JDBC 的数据访问异常纳入 Spring 自身的异常层次体系中,统一了数据接口的定义,简化了客户端代码对数据访问异常的处理。

因为 JdbcTemplate 主要是通过 模板方法模式对基于 JDBC 的数据访问代码进行统一封装,所以,在详细了解 JdbcTemplate 的实现之前,我们有必要先简单回顾一下模板方法模式。

1.模板方法模式简介

模板方法模式(Template Method Pattern)主要用于对算法或者行为逻辑进行封装,即如果多个类中存在某些相似的算法逻辑或者行为逻辑,可以将这些相似的逻辑提取到模板方法类中实现,然后让相应的子类根据需要实现某些自定义的逻辑。 举个例子来说,所有汽车,不管是宝马还是夏利,它们的驾驶流程基本是固定的。实际上,除了少数细节不同,大部分的流程是一样的,基本上是如下所示的流程说明:

(1)点火启动汽车;

(2)踩刹车,挂前进挡位(手动挡和自动挡这一步存在差异);

(3)放下手动制动器(俗称手刹);

(4)踩油门启动车辆运行。

那么,我们可以声明一个模板方法类,将确定的行为以模板的形式定义,而将不同的行为留给相应的子类实现。我们的模板类定义如下方代码清单所示。

    public abstract class Vehicle {
        public final void drive() {
            startTheEngine(); //点火启动汽车
            putIntoGear(); //踩刹车,挂前进挡位
            looseHandBrake(); //放下手动制动器
            stepOnTheGasAndGo(); //踩油门启动车辆运行
        }
 
        protected abstract void putIntoGear();
 
        private void stepOnTheGasAndGo() {
            // ...
        }
 
        private void looseHandBrake() {
            // ...
        }
 
        private void startTheEngine() {
        }
    }
 

drive()方法就是我们声明的模板方法,它声明为final,也就是说,方法内的逻辑是不可变更的。

车辆的入档因自动档车辆和手动挡车辆而有所不同,所以将putIntoGear()声明为抽象方法留给相应的具体子类实现,如下方代码清单中的子类定义。

 
 
    // 自动挡汽车
    public class VehicleAT extends Vehicle {
      @Override
      protected void putIntoGear() {
        // 挂前进挡位
      }
    }
 
    // 手动挡汽车
    public class VehicleMT extends Vehicle {
      @Override
      protected void putIntoGear() {
        // 踩离合器
        // 挂前进挡位
      }
    }
 
 

这样一来,就不需要每个子类中都声明并实现共有的逻辑,而只需要每个子类实现特有的逻辑就行了。

2.JdbcTemplate的演化

如果我们回头看一下最初的直接使用JDBC API进行数据访问的代码,就会发现,不管这些代码是由谁负责的,也不管这些代码所实现的数据访问逻辑如何,除了小部分的差异之外,所有这些代码几乎都是按照同样的一套流程下来的,如下。

(1)con=getDataSource().getConnection();取得数据库连接。

(2)stmt=con.createStatement();或者ps=con.prepareStatement(…);根据Connection创建相应的Statement或者PreparedStatement。

(3)stmt.executeUpdate(sql);或者ps.executeUpdate(…);根据传入的SQL语句或者参数,借助statement或者PreparedStatement进行数据库的更新或者查询。

(4)stmt.close();stmt=null;关闭相应的Statement或者PreparedStatement。

(5)catch(SQLExceptione){…}处理相应的数据库访问异常。

(6)finally{con.close();}关闭数据库连接以避免连接泄漏导致系统崩溃。

对于多个DAO中充斥的几乎相同的JDBCAPI的使用代码,我们也可以采用模板方法模式,对这些基于JDBCAPI的数据访问代码进行重构,杜绝因个人使用不当所导致的种种问题。

我们所要做的,只是将共有的一些行为提取到模板方法类中,而特有的操作,比如每次执行不同的更新,或者每次针对不同的查询结果进行不同的处理等行为,则放入具体子类中。这样,我们就有了一个JdbcTemplate的雏形(如下方代码清单所示)。

 
 
    public abstract class JdbcTemplate {
        public final Object execute(String sql) {
            Connection con = null;
            Statement stmt = null;
            try {
                con = getConnection();
                stmt = con.createStatement();
                Object retValue = executeWithStatement(stmt, sql);
                return retValue;
            } catch (SQLException e) {
                DataAccessException ex = translateSQLException(e);
                throw ex;
            } finally {
                closeStatement(stmt);
                releaseConnection(con);
            }
        }
 
        protected abstract Object executeWithStatement(Statement stmt, String sql);
        // ...其他方法定义
    }
 
 

这样处理之后,JDBC代码的使用得到了规范(进行数据访问的时候,每次使用的JDBC代码都几乎相同),异常处理和连接释放等问题也得到了统一的管理。但是,只使用模板方法模式还不足以提供方便的数据访问Helper类。顶着abstract帽子的JdbcTemplate作为Helper类,不能够独立使用暂且不说,让我们每次进行数据访问的时候都要给出一个相应的子类实现,这也实在太不地道了。

所以,Spring框架在实现JdbcTemplate的时候,除了使用模板方法模式之外,还引入了相应的 Callback接口定义,以避免每次使用该Helper类的时候都需要进行子类化。当引入称为StatementCallback的接口定义之后(如下所示):

 
 
    public interface StatementCa11back {
      ObjectdoWithStatement(Statementstmt);
    }
 
 

我们的JdbcTemplate就可以摆脱abstract的帽子,作为一个真正的Helper类而独立存在了(见下方代码清单)。

JdbcTemplate原型代码定义:

 
 
    public class JdbcTemplate {
        public final Object execute(StatementCallback callback) {
            Connection con = null;
            Statement stmt = null;
            try {
                con = getConnection();
                stmt = con.createStatement();
                Object retValue = callback.doWithStatement(stmt);
                return retValue;
            } catch (SQLException e) {
                DataAccessException ex = translateSQLException(e);
                throw ex;
            } finally {
                closeStatement(stmt);
                releaseConnection(con);
            }
        }
 
        // ...其他方法定义
    }
 
 

要在相应的DAO实现类中使用JdbcTemplate,只需要根据情况提供参数和相应的StatementCallback就行了,如下代码演示了JdbcTemplate原型的使用:

 
 
    JdbcTemplate jdbcTemplate = ...;
    final String sql = "update...";
    StatementCallback callback = new StatementCallback() {
      public Obejct doWithStatement(Statement stmt) {
        return new Integer(stmt.executeUpdate(sql));
      }
    };
    jdbcTemplate.execute(cal1back);
 
 

现在,开发人员只需要关心与数据访问逻辑相关的东西,对于JDBC底层相关的细节则不用过多地考虑。最主要的是,令人恼火的数据库连接没释放的问题,将再也不会来烦我们了。

到此为止,我们说的只是JdbcTemplate实现的中心思想。实际上,JdbcTemplate在实现的细节上要考虑许多的东西,所以,还是来看一下Spring中的JdbcTemplate到底是一个什么样的实现结构吧!

org.springframework.jdbc.core.JdbcTemplate的继承层次比较简单,如图14-1所示。

image-20220501194846873

org.springframework.jdbc.core.JdbcOperations接口定义界定了JdbcTemplate可以使用的JDBC操作集合,该接口提供的操作声明,从查询到更新无所不包,详情不在这里罗列了,可以查询该接口定义的Javadoc了解其中定义的所有可用的JDBC操作。

JdbcTemplate的直接父类是org.springframework.jdbc.support.JdbcAccessor,这是一个抽象类,主要为子类提供一些公用的属性,如下。

  • DataSource。javax.sql.DataSource是JDBC2.0之后引入的接口定义,用来替代基于java.sql.DriverManager的数据库连接创建方式。DataSource的角色可以看作JDBC的连接工厂(Connection Factory),具体实现可以引入 对数据库连接的缓冲池 以及 分布式事务支持 。所以,基本上javax.sql.DataSource现在应该作为获取数据库资源的统一接口。Spring数据访问层对数据库资源的访问,全部建立在javax.sql.DataSource标准接口之上,通过超类JdbcAccessor,JdbcTemplate自然也是以此为基准。

  • SQLExceptionTranslator。Spring将对SQLException的转译这一工作,抽象为特定的接口,也就是org.springframework.jdbc.support.SQLExceptionTranslator。我们将在稍后,对Spring框架中如何实现SQLException到其统一的数据访问异常体系的转译做详细介绍,现在只需要明白,通过超类JdbcAccessor定义的有关设置或者获取SQLExceptionTranslator的方法,JdbcTemplate可以在处理SQLException的时候,委托具体的SQLExceptionTranslator实现类来进行SQLException的转译。

这样,在JdbcAccessor的支持下,JabcTemplate就开始为了实现JabcOperations所规定的目标而努力奋斗了!

JdbcTemplate中各种模板方法按照其通过相应Callback接口所公开的API自由度的大小,可以简单划分为如下4组。

  • 面向Connection的模板方法。属于这一组的模板方法通过org.springframework.jdbc.core.ConnectionCallback回调接口所公开的java.sql.Connection进行数据访问。虽然关于Connection的获取和释放不需要关心,但通过ConnectionCallback所公开的API使用自由度还是很大,所以,除非特殊情况,比如,集成遗留系统的数据访问,通常情况下,应避免直接使用面向Connection层面的模板方法进行数据访问。
  • 面向Statement的模板方法。面向Statement的模板方法主要处理基于静态的SQL的数据访问请求。该组模板方法通过org.springframework.jdbc.core.StatementCallback回调接口对外公开java.sql.Statement类型的操作句柄。该方式缩小了回调接口内的权限范围,但是提高了API使用上的安全性和便捷性。
  • 面向Preparedstatement的模板方法。对于使用包含查询参数的SQL请求来说,使用PreparedStatement可以让我们免于SQL注入的攻击。而在使用PreparedStatement之前,需要根据传入的包含参数的SQL对其进行创建,所以,面向Preparedstatement的模板方法会通过org.springframework.jdbc.core.PreparedStatementCreator回调接口公开Connection以允许PreparedStatement的创建。另外,PreparedStatement创建之后,会公开给org.spring-framework.jdbc.core.PreparedStatementCallback回调接口,以支持其使用PreparedStatement进行数据访问。
  • 面向Callablestatement的模板方法。JDBC支持使用CallalbeStatement进行数据库存储过程的访问。面向CallableStatement的模板方法会通过org.springframework.jdbc.core.CallableStatementCreator公开相应的Connection以便创建用于调用存储过程的CallableStatement。之后,再通过org.springframework.jdbc.core.CallableStatementCallback公开创建的CallableStatement操作句柄,实现基于存储过程的数据访问。

每一组中的模板方法都有一个核心的方法实现,其他属于同一组的重载的模板方法,会调用这个核心的方法实现来完成最终的工作。以面向Statement的模板方法分组为例,使用StatementCallback回调接口作为方法参数的execute()方法是这一组模板方法的核心实现(见下方代码清单)。

 
    // --- 摘自Spring框架JdbcTemplate源码 ---
    public Object execute(StatementCallback action) throws DataAccessException {
      Assert.notNull(action, "Callback object must not be null");
      Connection con = DataSourceUtils.getConnection(getDataSource());
      Statement stmt = null;
      try {
        Connection conToUse = con;
        if (this.nativeJdbcExtractor != null &&
            this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
          conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
        }
        stmt = conToUse.createStatement();
        applyStatementSettings(stmt);
        Statement stmtToUse = stmt;
        if (this.nativeJdbcExtractor != null) {
          stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
        }
        Object result = action.doInStatement(stmtToUBe);
        handlewarnings(stmt.getWarnings());
        return result;
      } catch (SQLException ex) {
        // Release Connection early, to avoid potential connection pool deadlock
        // in the case when the exception translator hasn't been initialized yet.
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, getDataSource());
        con = null;
        throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
      } finally {
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, getDataSource());
      }
    }
 

其他模板方法会根据自身的方法签名,构建相应的statementCallback实例以调用execute(StatementCallback)方法,如下方代码清单所示。

 
 
    // --- 摘自Spring框架JdbcTemplate源码 ---
    public void execute(final String sql) throws DataAccessException {
      if (logger.isDebugEnabled()) {
        logger.debug("Executing SQL statement [" + sql + "]");
      }
 
      class ExecuteStatementCallback inplements StatementCallback,SqlProvider {
        public Object doInStatement(Statement stmt) throws SQLException {
          stmt.execute(sql);
          return null;
        }
        public String getSql() {
          return sql;
        }
      }
      execute(new ExecuteStatementCallback());
    }
 
 

同一组内的模板方法,可以根据使用的方便性进行增加,只要在实现的时候,将相应条件以对应该组的回调接口进行封装,最终调用当前组的核心模板方法即可。

3.使用DataSourceutils进行Connection的管理

如果你稍微关注JdbcTemplate的实现代码,就会发现JdbcTemplate在取得具体的数据库Connection的时候,不是从相应的DataSource中通过getConnection()方法直接取得可用的Connection对象,如以下代码所示:

 
 
    Connectioncon=dataSource.getConnection();
 

而是使用了一个DataSourceUtils工具类从指定的DataSource中取得相应的Connection,如以下代码所示:

 
    Connectioncon=DataSourceUtils.getConnection(getDataSource());

这是为什么呢?

实际上,如果我们要实现一个功能单一的JdbcTemplate的话,通过DataSource来getConnection()是完全可以的。只不过,Spring所提供的JdbcTemplate要关注更多的东西,所以,在从DataSource中取得连接的时候需要多做一点儿事情而已。

org.springframework.jdbc.datasource.DataSourceUtils提供相应的方法,用来从指定的DataSource获取或者释放连接。与直接从DataSource取得Connection不同, DataSourceUtils会将取得的Connection绑定到当前线程,以便在使用Spring提供的统一事务抽象层进行事务管理的时候使用。

有关Spring中统一的事务抽象概念将在第15章中详细阐述,现在只需要知道,使用DataSourceUtils作为Helper类从DataSource中取得Connection的方式,基本上比直接从DataSource中取得Connection的方式就多了这些东西

4.使用NativeJdbcExtractor来获得“真相”

对于DataSource实现来说,特别是J2EE应用服务器所提供的DataSource实现,出于事务管理或者其他有关资源管理的目的,当从这些DataSource实现中请求相应的Connection以及相关的statement的时候,它们会返回对应最初Connection以及statement对象的代理对象。

这么一处理,我们就得忽略各种Connection的特性,而只能以java.sql.Connection接口定义的方式使用它。可是,如果我们要获得数据库驱动程序提供的原始Connection实现类(例如,oracle.jdbc..OracleConnection),以便使用特定于数据库的特色功能的话,使用DataSource所返回的代理类将无法做到。

所以,这也就是为什么JdbcTemplate在使用具体的Connection或者statement之前,会首先检查是否需要使用驱动程序提供的具体实现类,而不是相应的代理对象(如以下代码所示):

 
 
    if (this.nativeJabcExtractor != null {
      //Extract native JDBCConnection,castable to OracleConnection or the like.
      conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
    }
    ...
 
    if (this.nativeJdbcExtractor != null) {
      stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
    }
    ...
 

JdbcTemplate内部定义了一个NativeJdbcExtractor类型的实例变量,当我们想要 使用数据库驱动所提供的原始API的时候,可以通过JdbcTemplate的setNativeJabcExtractor(NativeJdbcExtractor)方法设置相应的NativeJdbcExtractor实现类。这样,设置后的NativeJdbcExtractor实现类将负责剥离相应的代理对象,取得真正的目标对象供我们使用。

5.控制JdbcTemlate的行为

JdbcTemplate在通过statement或者PreparedStatement等进行具体的数据操作之前,会调用如下所示的代码:

applyStatementSettings(stmt);

或者

applyStatementSettings(ps);

或者

applyStatementSettings(cs);

这行代码有什么用处呢?

通过该方法,我们可以控制查询的一些行为,比如控制每次取得的最大结果集,以及查询的超时时间(timeout)等。

6.SQLException到DataAcceseException体系的转译

因为JdbcTemplate直接操作JDBCAPI,所以它需要捕获在此期间可能发生的SQLException并进行处理,处理的宗旨是将SQLException转译到Spring的数据访问异常层次体系,以统一数据访问异常的处理方式。

JdbcTemplate将SQLException转译到Spring数据访问异常层次体系这部分工作转交给了org.springframework.jdbc.support.SQLExceptionTranslator接口来完成,该接口的定义如下:

 
 
    public interface SQLExceptionTranslator {
      DataAccessException translate(Stringtask,Stringsql,SQLExceptionex);
    }
 
 

该接口有两个主要实现类,分别为org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslatororg.springframework.jabc.support.sQLStateSQLExceptionTranslator,如图14-2所示。

image-20220501215256816

更多不再介绍 有兴趣可以看原文