第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实现代码。
而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)踩油门启动车辆运行。
那么,我们可以声明一个模板方法类,将确定的行为以模板的形式定义,而将不同的行为留给相应的子类实现。我们的模板类定义如下方代码清单所示。
drive()方法就是我们声明的模板方法,它声明为final,也就是说,方法内的逻辑是不可变更的。
车辆的入档因自动档车辆和手动挡车辆而有所不同,所以将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的雏形(如下方代码清单所示)。
这样处理之后,JDBC代码的使用得到了规范(进行数据访问的时候,每次使用的JDBC代码都几乎相同),异常处理和连接释放等问题也得到了统一的管理。但是,只使用模板方法模式还不足以提供方便的数据访问Helper类。顶着abstract帽子的JdbcTemplate作为Helper类,不能够独立使用暂且不说,让我们每次进行数据访问的时候都要给出一个相应的子类实现,这也实在太不地道了。
所以,Spring框架在实现JdbcTemplate的时候,除了使用模板方法模式之外,还引入了相应的 Callback接口定义,以避免每次使用该Helper类的时候都需要进行子类化。当引入称为StatementCallback的接口定义之后(如下所示):
我们的JdbcTemplate就可以摆脱abstract的帽子,作为一个真正的Helper类而独立存在了(见下方代码清单)。
JdbcTemplate原型代码定义:
要在相应的DAO实现类中使用JdbcTemplate,只需要根据情况提供参数和相应的StatementCallback就行了,如下代码演示了JdbcTemplate原型的使用:
现在,开发人员只需要关心与数据访问逻辑相关的东西,对于JDBC底层相关的细节则不用过多地考虑。最主要的是,令人恼火的数据库连接没释放的问题,将再也不会来烦我们了。
到此为止,我们说的只是JdbcTemplate实现的中心思想。实际上,JdbcTemplate在实现的细节上要考虑许多的东西,所以,还是来看一下Spring中的JdbcTemplate到底是一个什么样的实现结构吧!
org.springframework.jdbc.core.JdbcTemplate
的继承层次比较简单,如图14-1所示。
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()方法是这一组模板方法的核心实现(见下方代码清单)。
其他模板方法会根据自身的方法签名,构建相应的statementCallback实例以调用execute(StatementCallback)方法,如下方代码清单所示。
同一组内的模板方法,可以根据使用的方便性进行增加,只要在实现的时候,将相应条件以对应该组的回调接口进行封装,最终调用当前组的核心模板方法即可。
3.使用DataSourceutils进行Connection的管理
如果你稍微关注JdbcTemplate的实现代码,就会发现JdbcTemplate在取得具体的数据库Connection的时候,不是从相应的DataSource中通过getConnection()方法直接取得可用的Connection对象,如以下代码所示:
而是使用了一个DataSourceUtils工具类从指定的DataSource中取得相应的Connection,如以下代码所示:
这是为什么呢?
实际上,如果我们要实现一个功能单一的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之前,会首先检查是否需要使用驱动程序提供的具体实现类,而不是相应的代理对象(如以下代码所示):
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
接口来完成,该接口的定义如下:
该接口有两个主要实现类,分别为org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
和org.springframework.jabc.support.sQLStateSQLExceptionTranslator
,如图14-2所示。
更多不再介绍 有兴趣可以看原文