Table of Contents
spring框架为了简化Java应用程序开发,提供了一个数据访问层,该数据访问层主要可以划分为三个部分:
-
统一的数据访问异常层次体系(Exception Hierarchy). spring框架将特定的数据访问技术相关的Exception进行转译,然后封装为一套标准的异常层次体系。 通过这套标准异常层次体系,不管使用的数据访问技术如何变化,客户端对象只需要捕获并处理这套标准的Exception就可以, 再也不需要因为所使用的数据访问技术变更或者迁移等问题而做任何改动。
-
JDBC[1] API的最佳实践. JDBC作为一套数据访问标准来说,是很成功的,他规范了各个数据库厂商之间的数据访问接口,极大的促进了RDBMS[2]在Java平台上的迅速普及。 但是,任何事物都有瑕疵,虽然JDBC作为一套标准来说很成功,但在JDBC API的设计和使用上则不尽然:
-
SQLException设计本身没有将自身作为标准的职责进行到底,各种异常信息全部放给了各个RDBMS厂商,从而导致应用程序需要根据数据库提供商的不同,来判定异常中所提供的信息具体是什么意思;
-
JDBC API较为贴近底层,使用上比较繁琐,如果不做合适的封装,在该API的使用上很容易造成问题,比如数据库连接没有释放就是最容易看到的情况;
-
-
以统一的方式对各种ORM方案的集成. 除了使用标准的JDBC进行数据库的访问,现在使用比较多的就是ORM,它的全称为“Object Relational Mapping”,或者称之为“对象-关系映射”, 主要用来屏蔽对象与关系数据库之间结构的非一致性。大部分的ORM API在使用上都贴近于JDBC API的使用风格, 所以,Spring也以“JDBC API的最佳实践”同样的方式对现有的各种ORM方案进行了集成,同时,将这些ORM特定的Exception纳入了它那套统一的异常层次体系;
现在,让我们先从“统一的数据访问异常层次体系”开始,探索一下Spring的数据访问层到底蕴含什么奥妙吧!
要了解Spring为什么要提供这么一套“统一的数据访问异常层次体系”,我们得先从DAO模式说起...
不管是一个逻辑简单的小软件系统还是一个关系复杂的大软件系统,都需要对系统的相关数据进行访问和存储,而这些数据的存储机制和访问方式往往随场景不同而各异。 为了统一和简化相关的数据访问操作,J2EE核心模式提出了DAO模式,即“Data Access Object”,中文通常称为“数据访问对象”。 通过DAO模式,我们可以完全将数据的访问方式和数据的存储机制相分离,很好的屏蔽掉了各种数据访问方式的差异性。 不论你数据是存储在普通的文本文件或者是csv文件,甚至关系数据库(RDBMS)或者LDAP(Lightweight Directory Access Protocol)系统中, 使用DAO模式,数据访问的客户端代码可以完全忽视这种差异,而以统一的接口来访问相应数据。
空话多说无益,我们还是来看一个具体的应用DAO模式的场景吧!
我想,对于大部分软件系统来说,顾客信息的数据访问是大家经常接触的吧?我们就以这对顾客信息的数据访问为例,看一下使用DAO模式如何运作的。 使用DAO模式对顾客信息进行数据访问,我们首先需要声明一个数据访问对象接口定义:
public interface ICustomerDao { Customer findCustomerByPK(String customerId); void updateCustomerStatus(Customer customer); //... }对于客户端代码,比如通常的Service层代码,只需要声明依赖该数据访问接口即可,所有的数据访问全部通过该接口进行, 即使以后因为数据存储机制发生变化而导致DAO接口实现类发生变化,客户端代码也不需要做任何的调整。(当然,如果设计不良,依然会存在需要调整客户端代 码的情况)
public class CustomerService { private ICustomerDao customerDao; public void disableCustomerCampain(String customerId) { Customer customer = getCustomerDao().findCustomerByPK(customerId); customer.setCampainStatus(CampainStatus.DISABLE); getCustomerDao().updateCustomerStatus(customer); } public ICustomerDao getCustomerDao() { return customerDao; } public void setCustomerDao(ICustomerDao customerDao) { this.customerDao = customerDao; } }通常情况下,顾客信息大都存储于关系数据库当中,所以,相应的,我们会提供一个基于JDBC的DAO接口实现类:
public class JdbcCustomerDao implements ICustomerDao { public Customer findCustomerByPK(String customerId) { // TODO Auto-generated method stub return null; } public void updateCustomerStatus(Customer customer) { // TODO Auto-generated method stub } }可能随着系统需求的变更,我们的顾客信息需要转移到LDAP服务,或者转而使用其他企业位于LDAP服务中的顾客信息,又或者别人要使用我们的CustomerService,但他们使用不同的数据存储机制, 这个时候,就需要提供一个基于LDAP的数据访问对象:
public class LdapCustomerDao implements ICustomerDao { public Customer findCustomerByPK(String customerId) { // TODO Auto-generated method stub return null; } public void updateCustomerStatus(Customer customer) { // TODO Auto-generated method stub } }即使数据访问接口实现类随着需求发生变化,可是客户端代码(这里的CustomerService)却可以完全忽视这种变化, 唯一需要变动的地方可能只是Factory对象的几行代码甚至只是IoC容器配置文件中简单的class类型替换而已,而客户端代码无需任何变动。 所以,DAO模式对屏蔽不同数据访问机制的差异性起到举足轻重的作用。
但是,图画真的像我描述的那么美好吗?!
其实,之前针对DAO的例子为了简化概念的描述,我们省略了部分细节,比如接口与实现之间的某种"依赖性"。不管是JdbcCustomerDao 还是LdapCustomerDao,我们都省略了最基本的东西-数据访问机制特定的代码。 当我们把这些特定于数据访问机制的代码引入的时候,问题就产生了,最明显的莫过于特定于数据访问机制的异常处理。
当我们把具体的JDBC代码充实到我们的JdbcCustomerDao的时候,看看哪里不对头那:
public Customer findCustomerByPK(String customerId) { Connection con = null; try { con = getDataSource().getConnection(); //... Customer cust = ...; return cust; } catch (SQLException e) { // throw or handle it here? } finally { releaseConnection(con); } } private void releaseConnection(Connection con) { // TODO Auto-generated method stub }使用JDBC进行数据访问,当期间出现问题的时候,JDBC API会通过抛出SQLException的方式来表明问题的发生,而SQLException属于“checked exception”, 所以,我们的DAO实现类需要捕获这种异常,并进行处理。
那么,该DAO实现类中捕获的SQLException进行如何的处理那?直接在DAO实现类处理掉?可是这样的话,客户端代码如何得知在数据访问 期间发生了问题? 所以,我们只好先直接将SQLException抛给客户端,进而,DAO实现类的相应方法签名也需要修正为抛出SQLException:
public Customer findCustomerByPK(String customerId) throws SQLException { ... }相应的,DAO接口定义中的相应方法签名也需要修改:
public interface ICustomerDao { Customer findCustomerByPK(String customerId) throws SQLException; void updateCustomerStatus(Customer customer); //... }可是,这样就解决问题了吗?
我们的数据访问接口对于客户端来说是通用的,不管我们的数据访问对象因为数据访问机制的不同而如何变更,客户端代码不应该受其牵连。 但是,现在因为使用JDBC做数据访问需要抛出特定的SQLException,客户端代码就需要捕捉该异常并做相应的处理。这是与数据访问对象模式的设 计初衷相背离的。
当我们引入另一种数据访问机制的时候,问题更是接踵而来。当我们再加入LdapCustomerDao实现的时候,LdapCustomerDao 需要抛出NamingException, 如果要保证findCustomerByPK方法是实现(implements)了ICustomerDao中的方法,那么我们就得更改 ICustomerDao的方法签名:
Customer findCustomerByPK(String customerId) throws SQLException,NamingException;糟糕不是吗?我们又把统一的访问接口给改了,相应的客户端代码又要捕捉NamingException做相应的处理,如果随着不同数据访问对象实现的增多,以及考虑数据访问对象中其他数据访问方法,这种糟糕的情况不得继续下去吗?
问题出现了,我们就应该尝试解决问题,因为数据访问对象模式所描述的场景我们实在不忍舍弃。 那么如何来避免以上问题那?
-
既然将SQLException在DAO实现类内部直接处理掉这条路走不通,而将SQLException直接抛出又不可行,那我们将SQLException或者其他特定的数据访问异常进行封装后再抛出又会如何? 那么,以什么类型的异常进行封装然后再抛出那?是“checked exception”还是“unchecked exception”?
大部分的或者说所有的数据访问操作,其抛出的异常对于客户端来说属于系统的Fault,客户端是无法有效处理的,比如数据库操作失败,无法 取得相应资源等, 客户端对于这些情况最有效方式就是不做处理,因为客户端代码通常对于系统的Fault也无法处理(当然如果必要,捕捉后处理也是可以的,比如捕捉相应异常 后重试等), 所以,将SQLException以及其他特定于数据访问机制的异常以“unchecked exception”进行封装然后抛出是比较合适的。
因为“unchecked exception”不需要编译器检查,ICustomerDao的数据访问方法就可恢复其本来面目而实现“大同”:
Customer findCustomerByPK(String customerId)
各个DAO实现类内部只要将SQLException及其他特定的数据访问异常以“unchecked exception”进行封装:public Customer findCustomerByPK(String customerId) { Connection con = null; try { con = getDataSource().getConnection(); //... Customer cust = ...; return cust; } catch (SQLException e) { throw new RuntimeException(e); } finally { releaseConnection(con); } }
现在,统一数据访问接口定义的问题解决! -
以单一的RuntimeException形式将特定的数据访问异常转换后抛出虽然解决了统一数据访问接口的问题,但是,该方案依然不够周 全。 以SQLException为例,各个数据库提供商通过SQLException来表达具体的错误信息的时候,所采用的方式是不同的,比如,有的数据库提 供商采用SQLException的ErrorCode作为具体的错误信息标准, 有的数据库提供商则通过SQLException的SqlState来返回详细的错误信息,即使我们将SQLException封装后抛出给客户端对象, 当客户端对象需要了解具体的错误信息的时候, 依然需要根据数据库提供商的不同,采取不同的信息提取方式,要知道,将这种错误信息的具体处理分散到各个客户端对象中处理又是何等的糟糕?!我们应该向客 户端对象屏蔽这种差异性!
那么,如何来屏蔽这种差异性那?答案当然是异常的分类转译(Exception Translation)!
-
首先,我们不应该将对特定的数据访问异常的错误信息提取工作下放给客户端对象进行处理,而是应该由DAO实现类或者某个工具类以统 一的方式进行处理。 我们暂且让具体的DAO实现类来做这个工作吧,那么对于我们的JdbcCustomerDao来说,捕获异常后的处理就类似于:
try { //... } catch (SQLException e) { if(isMysqlVendor()) { // 按照Mysql数据库的规则分析错误信息(e)然后抛出 throw new RuntimeException(e); } if(isOracleVendor()) { // 按照Oracle数据库的规则分析错误信息(e)然后抛出 throw new RuntimeException(e); } ... }
-
啊哈!信息是提取出来了,可是,单单通过RuntimeException一个异常类型还不足以区分不同的错误类型,我们需要将数 据访问期间发生的错误进行分类,然后为具体的错误分类分配一个对应的异常类型。 比如,数据库连接不上,ldap服务器连接失败,我们认为他们同属于“获取资源失败”这种情况,而主键冲突或者其他资源冲突,我们认为他们属于“数据一致性冲突”, 那么,针对这些情况,我们就可以以RuntimeException为基准,为“获取资源失败”这种情况分配一个RuntimeException的子类型,比如称其为“ResourceFailureException”, 而“数据一致性冲突”则可以对应RuntimeException的另一个子类型“DataIntegrityViolationException”,其他的分类和异常类型以此类推,这样,我们就有了以下的异常处理逻辑:
try { //... } catch (SQLException e) { if(isMysqlVendor()) { if(1==e.getErrorCode()) throw new ResourceFailureException(e); else if(1062 == e.getErrorCode()) throw new DataIntegrityViolationException(e); else ...; } if(isOracleVendor()) { int[] resourceFailureCodes = {17002,17447}; int[] dataIntegrationViolationCode = {1,1400,1722,2291}; ... if(ArrayUtils.contains(resourceFailureCodes,e.getErrorCode())) throw new ResourceFailureException(e); else if(ArrayUtils.contains(dataIntegrationViolationCode,e.getErrorCode())) throw new DataIntegrityViolationException(e); else ...; } ... }
不论你采用的是什么数据库服务器,也不论你采用的是什么数据访问方式, 不单单是这里实例所提到的基于JDBC的数据访问方式,对于其他的数据访问方式来说,只要将他们自身的异常通过某种方式转译为以上提到的这几种类型的异常 类型, 对于客户端对象来说,则只需要关注这几种类型的异常就可以知道到底出了什么问题,甚至系统监控人员也可以直接根据日志信息判断问题之所在。说到底,当一套语义完整的异常体系定义完成之后,不管数据访问方式如何变换,只要相应的数据访问方式能够将自身的异常转译到这套语义完整的异常体系定义之内, 对于客户端对象来说,自身的数据访问异常处理逻辑从此就是岿然不动的。
-
实际上,我们需要的仅仅就是一套“unchecked exception”类型的面向数据访问领域的异常层次体系。
一套“unchecked exception”类型的面向数据访问领域的异常层次体系存在的必要性是有了,我们马上着手设计和实现它? NO,我们已经有了现成的“轮子”啦!那就是Spring提供的数据访问异常层次体系!
spring框架中统一的异常层次体系所牵扯的大部分异常类型定义位于org.springframework.dao包下面, 位于这个体系的所有异常类型均以org.springframework.dao.DataAccessException为统领,然后根据职能划分不同 的异常子类型,总体上看,整个的异常层次体系如下图:
下面我们具体来看位于这个异常层次体系的各种异常类型定义具体职责是啥:
-
CleanupFailureDataAccessException. 当我们已经成功完成相应的数据访问操作,要对使用的资源进行清理却失败的时候,将抛出该异常。 比如,我们使用JDBC进行数据访问的时候,查询或者更新数据操作完成之后,需要关闭相应的数据库连接,如果在关闭连接的过程中出现 SQLException,则数据库连接没有被释放,导致资源清理失败。
-
DataAccessResourceFailureException. 当无法访问相应的数据资源的情况下,将抛出DataAccessResourceFailureException。 对应这种异常出现的最常见的场景就是数据库服务器挂掉的情况,这时候,连接数据库的应用程序可以通过捕获该异常了解到是数据库服务器端出现问题。 对于JDBC来说,当数据库服务器挂掉之后,对应会抛出DataAccessResourceFailureException针对JDBC的具体子类 型,即org.springframework.jdbc.CannotGetJdbcConnectionException。
-
DataSourceLookupFailureException. 当我们尝试对JNDI[3]服务上或者其他位置上的DataSource进行查找,而查找失败的时候,可以抛出DataSourceLookupFailureException。
-
ConcurrencyFailureException. 并发进行数据访问操作失败的时候,可以抛出ConcurrencyFailureException,比如无法取得相应的数据库锁或者乐观锁更新冲突等情 况。 ConcurrencyFailureException下面根据不同的并发数据访问失败的情况,细分为多个子类型:
-
InvalidDataAccessApiUsageException. 该异常的出现通常不是因为数据资源出现了问题,而是当我们以错误的方式使用了特定的数据访问API的时候,会抛出该异常。 比如,你使用Spring的JdbcTemplate的getForObject()方法进行查询操作,而你传入的SQL查询却可能返回多行结果的时候, 就会抛出该异常, 因为getForObject()语义上只返回单一的结果对象,你应该使用能够返回多行记录的查询方法,而不是只能返回单一结果的 getForObject()方法。
-
InvalidDataAccessResourceUsageException. 以错误的方式对数据资源进行访问的时候会抛出InvalidDataAccessResourceUsageException,比如,要对数据库资源进 行访问,而却传入错误的SQL,或者以其他错误方式对数据库进行访问的时候。 各种数据访问方式会根据自身情况,抛出InvalidDataAccessResourceUsageException的子类以进一步区分详细的错误情 况,比如,基于JDBC的数据访问会通过抛出org.springframework.jdbc.BadSqlGrammarException以表示访 问操作传入了错误格式的SQL;而基于Hibernate的数据访问会通过抛出HibernateQueryException以表示访问操作传入了的 HQL语法有问题。 InvalidDataAccessResourceUsageException的特定子类由相应的数据访问实现方式提供。
-
DataRetrievalFailureException. 如果要获取预期的数据却失败的时候,会抛出DataRetrievalFailureException。比如,已知某顾客存在,你要根据该顾客号获取顾客信息却失败了的时候,可以抛出该异常。
-
PermissionDeniedDataAccessException. 尝试访问某些数据,而自身却没有相应权限的情况下,将抛出该异常。如果你所使用的用户没有被授予相应权限而你却尝试进行权限之外的操作的时候,就会导致PermissionDeniedDataAccessException的发生。
-
DataIntegrityViolationException. 顾名思义,数据一致性冲突异常是在你尝试更新数据却违反了数据一致性检查的情况下将会抛出的异常,比如数据库中已经存在主键为1的记录,你又尝试插入同样 主键记录的时候,无疑将触发该异常。 通常系统中抛出DataIntegrityViolationException表示系统的哪个部分出现了问题,这个时候需要相关人员来查找到底哪个地方 出现了问题;但也不排除说,抛出DataIntegrityViolationException之后, 可以忽略这种冲突的情况,而将插入操作改为更新操作的情况。
假设系统逻辑允许,在某个场景中,当插入某条记录却出现主键冲突的时候,改为更新记录,那么,我们就可以捕获 DataIntegrityViolationException然后进行处理,而不是像通常的DataAccessException那样程序中不做捕 获也不做处理:
try { // do data access here // insert record failed because of pk violation } catch(DataIntegrityViolationException e) { logger.warn("PK Violation With...."); // do update instead of insertion }
-
UncategorizedDataAccessException. 其他无法详细分类的数据访问异常情况下,可以抛出UncategorizedDataAccessException。该异常定义为abstract,如 果对于特定的数据访问方式来说,以上的异常类型无法描述当前数据访问方式中特定的异常情况的话, 可以通过扩展UncategorizedDataAccessException来进一步细化特定的数据访问异常类型。
不管怎么样,到此为止,我想您已经对于Spring为什么要提供这么一套标准的异常层次体系以及如何发挥这套异常层次体系的最大作用了然于心了吧?! 那么让我们继续前进,开始了解Spring提供的“JDBC API的最佳实践”!
Spring提供了两种使用JDBC API的最佳实践方式,一种是以JdbcTemplate为核心的“基于Template的Jdbc使用方式”,另一种则是在JdbcTemplate基础之上构建的“基于操作对象的Jdbc使用方式”。
下面让我们先从“基于Template的Jdbc使用方式”开始看起...
基于Template的Jdbc使用方式的最初设想和原型需要追溯到Rod Johnson在2003年出版的《Expert One-on-One J2EE Design and Development》一书, 在该书的“数据访问实践(Practical Data Access)”一章中,Rod针对Jdbc使用中的一些问题提出了一套改进的实践原型,并且最终将该原型完善后在Spring框架中发布。
下面,是我们对这段旅程的再次回顾...
JDBC作为Java平台访问关系数据库的标准API,其成功是我们所有目共睹的,几乎所有Java平台的数据访问都直接的或者间接的使用了JDBC,它是整个Java平台面向关系数据库进行数据访问的基石。
作为一个标准来说,JDBC无疑是成功的;但是,你要说JDBC在实际的使用过程中也是受人欢迎的,则不尽然了。 JDBC标准主要面向较为底层的数据库操作,所以,在API的设计过程中过于贴近底层以提供尽可能多的功能特色,从这个角度来说,JDBC API的设计是无可厚非的。 可是,过于贴近底层的API设计,对于开发人员的使用来说,就不一定是好事了,即使进行简单的查询或者更新,开发人员也要按部就班的按照API的“规矩”写上一大堆的雷同的代码, 如果不能对JDBC API的使用进行合理的封装使用,项目中使用JDBC进行数据访问所出现的问题估计会让你“抓狂”。
对于通常的项目开发来说,如何层次划分明晰,数据访问逻辑通常在相应的DAO中实现,因为功能模块的划分,每个开发人员可能都会分得或多或少的实现相应DAO的任务,假设A分得了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实现没有问题,可你无法保证其他开发人员也能够如此,我想,下面 的代码你或多或少都经历过吧:
Connection con = null; try { con = getDataSource().getConnection(); Statement stmt1 = con.createStatement(); ResultSet rs1 = stmt1.executeQuery(sql); while(rs1.next()) { String someValue = rs.getString(1); Statement stmt2 = con.createStatement(); ResultSet rs2 = stmt2.executeQuery(sql2); while(rs1.next()) { String innerValue = rs2.getString(); ... } rs2.close(); rs2 = null; stmt2.close(); stmt2 = null; } rs1.close(); rs1 = null; stmt1.close(); stmt1 = null; } catch(SQLException e) { throw new DaoException(e); } finally { if(con != null) { try { con.close(); } catch(SQLException e) { logger.warn("failed to close Connection:+"+ex); } } }几乎程式一样的代码先不说,JDBC驱动程序是否支持嵌套的结果集我也先放一边不提,这一段代码里面多个Statement和多个ResultSet的情 况,看着是否是曾相识那? 呵呵,可能现在都从使用ORM开始了,很少直接写JDBC代码了,不过,我得承认,我见过甚至深恶痛绝这样的代码,即使你要使用多个Statement和 多个ResultSet来完成一个功能, 即使JDBC驱动程序也允许你这么做,但往往错误出在不经意之间,就跟上面的代码一样,你明明要对rs2进行遍历,却鬼使神差的写成了rs1,Wow,真 是一个不小的“惊喜”!
这其实只是API的使用过程中的一处小插曲,当你看看应用程序中几十上百的使用JDBC的DAO实现的时候,我可以保证你能够发现更多的“惊喜”:
-
Statement使用完后没关闭,而想着让Connection关闭的时候一并关闭,你倒是省事儿了,可是并非所有的驱动程序都有这样的行为;
-
创建了多个ResultSet或者Statement,最后却只清理了最外层的,而忽视了最里层的;
-
好不容易忙活完try字句里面的数据访问逻辑,却完全忽视了使用的Connection还没有释放;
这个时候你会说,“JDBC用起来可真烦人!”,嗯,不得不承认,确实如此。
不过,除了API的使用,JDBC规范在制定数据访问异常处理的时候也没能够“将革命进行到底”。
-
最初的JDBC规范中通过单一的SQLException类型来囊括一切数据访问异常情况,将SQLException声明为“checked exception”合适与否我们之前已经提过, 显然这是需要改进的一个地方,而最新版本的JDBC规范也就这一点进行了改进,不过这是后话;
-
除此之外,SQLException没有采用将具体的异常情况子类化以进一步抽象不同的数据访问异常情况,而是采用ErrorCode的方 式来区分数据访问过程中所出现的不同异常情况, 其实,这也没啥,只要能够区分具体错误情况就行,但是,JDBC规范却把ErrorCode的规范制定下放给了各个数据库提供商(当然,可能并非有意,或 者有啥难言之隐),导致一个数据库对应一套ErrorCode,进而应用程序在捕获SQLException之后,还要先瞅瞅当前使用的是啥数据库, 然后再从SQLException中通过getErrorCode()取得相应ErrorCode与数据库厂商提供的ErrorCode列表进行对比才能 搞清楚到底哪里出了问题。如果当初JDBC规范能够规定死“ErrorCode=1代表数据库连接不上,ErrorCode2=代表要访问的表不存在...”, 我想现在“群雄逐鹿”的局面也不会让我们在处理SQLException的时候如此痛苦了。
Note
Java平台从1.1开始引入JDBC,迄今已经经历了几个大的版本变动, 标准的制定也吸取了spring框架的JDBC抽象层的部分经验,比如对数据访问异常的层次体系的处理方面,你可以在最新的JDBC规范中看到这些改进后的亮点。
针对JDBC API在使用中容易出错,使用繁琐的问题,以及在SQLException对数据访问异常处理能力不足尚待改进的情况之下,Spring框架提出了一套针 对JDBC使用方面的框架类,以改进JDBC API使用过程中的种种不便甚至不合理之处,帮助我们进一步提高开发过程中使用JDBC进行数据访问的开发效率。
为了解决JDBC API在实际使用中的种种尴尬局面,Spring框架提出了org.springframework.jdbc.core.JdbcTemplate作为 数据访问的Helper类。 JdbcTemplate是整个Spring数据抽象层提供的所有JDBC API最佳实践的基础,框架内其他更加方便的Helper类以及更高层次的抽象全部构建于JdbcTemplate之上。抓住JdbcTemplate, 你就算抓住了Spring框架JDBC API最佳实践之灵魂。
概括点儿来说,JdbcTemplate主要关注两个事情[4]:
-
封装所有基于JDBC的数据访问代码,以统一的格式和规范来使用JDBC API,现在所有基于Jdbc的数据访问需求现在全部通过JdbcTemplate进行, 从而避免了让繁琐易错的基于JDBC API的数据访问代码散落于系统各处;
-
对SQLException所提供的异常信息在框架内统一进行转译,将基于Jdbc的数据访问异常纳入Spring自身的异常层次体系当中,统一了数据接口的定义,简化了客户端代码对数据访问异常的处理;
模板方法模式主要用于对算法或者行为逻辑进行封装,如果多个类中存在某些相似的算法逻辑或者行为逻辑,可以通过将这些相似的逻辑提取到模板方法类中实现,然后让相应的子类根据需要实现某些自定义的逻辑。
举个例子来说,所有汽车的驾驶基本是固定的,不管他是宝马还是夏利,实际上,除了少数细节不同,大部分的流程是一样的:
那么,我们可以声明一个模板方法类,将确定的行为以模板的形式定义,而将不同的行为下放给相应的子类实现:
public abstract 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() { // 踩离合器 // 挂前进挡位 } }这样一来,就不需要每个子类中都声明并实现共有的逻辑,而只需要实现特有的逻辑就行了。
如果我们回头看一下最初的直接使用JDBC API进行数据访问的代码,你会发现,不管这些代码是由谁负责的,也不管这些代码所实现的数据访问逻辑如何,除了小部分的差异之外,所有这些代码几乎都是按照同样的一套流程下来的:
取得数据库连接;
根据Connection创建相应的Statement或者PreparedStatement;
根据传入的SQL语句或者参数借助Statement或者PreparedStatement进行数据库的更新或者查询;如果是查询操作,则对结果集进行遍历,抽取查询后的结果;
ResultSet rs = stmt.executeQuery(sql); while(rs.next()) { processResultRow(rs); }
关闭相应的Statement或者PreparedStatement;
处理相应的数据库访问异常;
关闭数据库连接以避免连接泄漏导致系统crash;
对于多个DAO中充斥的几乎相同的JDBC API的使用代码,我们也可以采用“模板方法模式(Template Method Pattern)”对这些基于JDBC API的数据访问代码进行重构,以杜绝因个人使用不当所导致的种种问题。 我们所要做的,仅仅是将共有的一些行为提取到模板方法类中,而特有的操作,比如每次执行不同的更新,或者每次针对不同的查询结果进行不同的处理等行为,则放入具体子类中:
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代码使用得到了规范,异常处理和连接释放等问题也得到了统一的管理,但是,只是使用模板方法模式还 不足以提供方便的数据访问Helper类, 顶着abstract帽子的JdbcTemplate作为Helper类不能够独立使用不说,每次进行数据访问都要给出一个相应的子类实现,这也实在太不 地道了。
所以,Spring框架在实现JdbcTemplate的时候,除了使用模板方法模式之外,还引入了相应的Callback接口定义,以避免每次使用该Helper类的时候都需要去进行子类化。 当我们引入成为StatementCallback的接口定义之后:
public interface StatementCallback { Object doWithStatement(Statement stmt); }我们的JdbcTemplate就可以摆脱abstract的帽子,作为一个堂堂正正的Helper类而独立存在了:
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 = ...; final String sql = "update ..."; StatementCallback callback = new StatementCallback(){ public Obejct doWithStatement(Statement stmt) { return new Integer(stmt.executeUpdate(sql)); } }; jdbcTemplate.execute(callback);现在,开发人员只需要关心与数据访问逻辑相关的东西,对于JDBC底层相关的细节却不用过多的考虑,进而也不会出现令人恼火的数据库连接没释放的问题了。
Note
Callback接口与模板方法类之间的关系可以看作是服务与被服务的关系, 模板方法类想要Callback做事,就要提供相应的资源(在这里是通过doWithStatement方法暴露Statement给callback接 口),Callback使用提供的资源做事, 完事之后,模板方法类来处理暴露的资源,callback接口不需要关心这些。
实际上,Java中没有Ruby语言类似的Block的语法结构,所以,我们只好使用匿名内部类来完成同样的功能,不过匿名内部类也不算难用吧! 现在对于要不要在Java中引入Closure语法结构的讨论也在进行中,据说java7中将会引入对Closure的支持, 那个时候或许你就可以直接使用Closure语法来代替匿名内部类了。
不过那,到此为止,我们仅仅说的是JdbcTemplate实现的中心思想,实际上,JdbcTemplate在实现的细节上要考虑许多的东西,所以,还是来看一下Spring中的JdbcTemplate到底是一个什么样的实现结构吧!
org.springframework.jdbc.core.JdbcTemplate的继承层次比较简单,如下图:
org.springframework.jdbc.core.JdbcOperations接口定 义界定了JdbcTemplate可以使用的Jdbc操作集合,该接口从查询到更新无所不包,详情我们就不在这里罗列了,太长,你可以查询该接口定义的 javadoc了解其中所定义的所有可用的Jdbc操作; JdbcTemplate的直接超类是org.springframework.jdbc.support.JdbcAccessor,这是一个抽象类, 主要为子类提供一些公用的属性,包括:
-
DataSource. javax.sql.DataSource是JDBC 2.0之后引入的接口定义,用以替代基于java.sql.DriverManager的数据库连接创建方式。 DataSource的角色可以看作一个Jdbc的连接工厂(ConnectionFactory),具体实现可以引入对数据库连接的缓冲池以及分布式事 务支持。 所以,现在基本上javax.sql.DataSource应该作为获取数据库资源的统一接口。
Spring数据访问层的整个对数据库资源的访问,全部建立在javax.sql.DataSource标准接口之上,通过超类JdbcAccessor,JdbcTemplate自然也是以此为基准啦!
-
SQLExceptionTranslator. spring将对SQLException的转译抽象为特定的接口来负责,也就是 org.springframework.jdbc.support.SQLExceptionTranslator, 我们将在稍后对spring框架中如何实现SQLException到其统一的数据访问异常体系的转译做详细介绍,现在只需要明白,通过超类 JdbcAccessor定义的有关设置或者获取SQLExceptionTranslator的方法, JdbcTemplate在处理SQLException的时候,可以委托具体的SQLExceptionTranslator实现类来进行。
JdbcTemplate中各种模板方法按照其通过相应Callback接口所暴露的API自由度的大小,可以简单划分为四组:
-
面向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 Injection” 的攻击, 而PreparedStatement在使用之前需要根据传入的包含参数的SQL进行创建,所以,面向PreparedStatement的模板方法会通 过org.springframework.jdbc.core.PreparedStatementCreator回调接口暴露Connection以 允许PreparedStatement的创建; 另外,PreparedStatement创建之后,会通过 org.springframework.jdbc.core.PreparedStatementCallback暴露给回调接口,以支持使用 PreparedStatement进行的数据访问。
-
面向CallableStatement的模板方法. Jdbc支持使用CallalbeStatement进行数据库存储过程的访问,面向CallableStatement的模板方法会通过 org.springframework.jdbc.core.CallableStatementCreator暴露相应的Connection以便创 建用于调用存储过程的CallableStatement, 之后,再通过org.springframework.jdbc.core.CallableStatementCallback暴露创建的 CallableStatement操作句柄,以实现基于存储过程的数据访问。
// --- 摘自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(stmtToUse); 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)方法:
// --- 摘自JdbcTemplate源代码 --- public void execute(final String sql) throws DataAccessException { if (logger.isDebugEnabled()) { logger.debug("Executing SQL statement [" + sql + "]"); } class ExecuteStatementCallback implements StatementCallback, SqlProvider { public Object doInStatement(Statement stmt) throws SQLException { stmt.execute(sql); return null; } public String getSql() { return sql; } } execute(new ExecuteStatementCallback()); }同一组内的模板方法可以根据使用方便进行实现及追加,然后将相应条件以对应该组的回调接口进行封装,最终调用当前组的核心模板方法即可。
到此为止,JdbcTemplate算是功德圆满了。不过,要想知道JdbcTemplate实现中的更多细节,咱们接着看...
如果你稍微关注JdbcTemplate的实现代码的话,会发现JdbcTemplate在取得具体的数据库Connection的时候,不是直接从相应的DataSource中通过getConnection()方法直接取得可用的Connection对象:
Connection con = dataSource.getConnection();而是,使用了一个DataSourceUtils工具类从指定的DataSource中取得相应的Connection:
Connection con = DataSourceUtils.getConnection(getDataSource());这是为什么那?
实际上,如果我们要实现一个单一功能的JdbcTemplate的话,通过DataSource的getConnection()这种方式是完全可 以的,只不过,Spring所提供的JdbcTemplate要关注更多的东西, 所以,在从DataSource中取得连接的时候需要多做一点儿事情而已。
org.springframework.jdbc.datasource.DataSourceUtils提供相应的方法用来从指定的 DataSource获取或者释放连接,与直接从DataSource取得Connection不同, DataSourceUtils会将取得的Connection绑定到当前线程以便在使用Spring提供的统一事务抽象层进行事务管理的时候使用。有关 Spring中统一的事务抽象概念我们在下一章进行阐述, 所以,现在你只需要知道,使用DataSourceUtils作为Helper类从DataSource中取得Connection的方式,基本上比直接 从DataSource中取得Connection的方式就多了这些东西。
对于DataSource实现来说,特别是J2EE应用服务器所提供的DataSource实现,出于事务管理或者其他有关资源管理的目的, 当你从这些DataSource实现中请求相应的Connection以及相关的Statement的时候,他们会返回对应最初Connection以及 Statement对象的代理对象。
这么一处理,你就只能忽略各种Connection的特异性,而只能以java.sql.Connection接口定义的方式来使用它, 可是,如果我们要获得数据库驱动程序提供的原始Connection实现类(例如,oracle.jdbc.OracleConnection),以便使 用特定于数据库的特色的话,使用DataSource真正所返回的代理类将无法做到。
所以,这也就是为什么JdbcTemplate在使用具体的Connection或者Statement之前会首先检查是否需要使用驱动程序提供的具体实现类,而不是相应的代理对象:
if (this.nativeJdbcExtractor != null) { // Extract native JDBC Connection, castable to OracleConnection or the like. conToUse = this.nativeJdbcExtractor.getNativeConnection(con); } ... if (this.nativeJdbcExtractor != null) { stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt); } ...JdbcTemplate内部定义有一个NativeJdbcExtractor类型的实例变量,当你想要使用数据库驱动所提供的原始API的时候, 可以通过JdbcTemplate的“setNativeJdbcExtractor(NativeJdbcExtractor)”方法设置相应的NativeJdbcExtractor实现类,这样,设置后的NativeJdbcExtractor实现类将负责剥离相应的代理对象,取得真正的目标对象供我们使用。
Spring默认提供面向Commons DBCP,C3P0, Weblogic,Websphere等数据源的NativeJdbcExtractor实现类:
-
CommonsDbcpNativeJdbcExtractor. 为Jakarta Commons DBCP数据库连接池所提供的NativeJdbcExtractor实现类;
-
C3P0NativeJdbcExtractor. 为C3P0数据库连接池所提供的NativeJdbcExtractor实现类;
-
WebLogicNativeJdbcExtractor. 为Weblogic所准备的NativeJdbcExtractor实现类;
-
WebSphereNativeJdbcExtractor. 为WebSphere所准备的NativeJdbcExtractor实现类;
JdbcTemplate在通过Statement或者PreparedStatement等进行具体的数据操作之前,会调用以下代码:
applyStatementSettings(stmt); 或者 applyStatementSettings(ps); 或者 applyStatementSettings(cs);这行代码有何用处那?
实际上,通过该方法,我们可以控制查询的一些行为,比如控制每次取得的最大结果集,以及查询的超时时间(timeout):
protected void applyStatementSettings(Statement stmt) throws SQLException { int fetchSize = getFetchSize(); if (fetchSize > 0) { stmt.setFetchSize(fetchSize); } int maxRows = getMaxRows(); if (maxRows > 0) { stmt.setMaxRows(maxRows); } DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout()); }你可以通过相应的setter()方法对JdbcTemplate声明的fetchSize,maxRows以及queryTimeout属性设置,比 如,如果某个查询可能返回的结果集很大,一次抽取的话可能导致程序OutOfMemory,那么你就可以通过设置fetchSize来指定每次最多抽取 1000行:
JdbcTemplate jt = new JdbcTemplate(..); jt.setFetchSize(1000); // use jt to do sth.Easy, Right?
JdbcTemplate因为直接操作Jdbc API,所以,它需要捕获在此期间可能发生的SQLException并进行处理,处理的宗旨当然就是将SQLException转译到Spring的数据访问异常层次体系,以统一数据访问异常的处理方式。
JdbcTemplate将“SQLException转译到Spring数据访问异常层次体系”这部分工作转交给了org.springframework.jdbc.support.SQLExceptionTranslator接口来完成,该接口的定义如下:
public interface SQLExceptionTranslator { DataAccessException translate(String task, String sql, SQLException ex); }该接口有两个主要实现类,分别为 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator和 org.springframework.jdbc.support.SQLStateSQLExceptionTranslator:
其中,SQLExceptionSubclassTranslator是spring2.5新追加的 SQLExceptionTranslator实现类,主要用于将随JDK6发布的JDBC4版本中新定义的异常体系转化到Spring的数据访问异常体 系,对于之前的版本,该实现类自然是用不上啦。
SQLErrorCodeSQLExceptionTranslator会基于SQLException所返回的ErrorCode进行异常转译, 通常情况下,根据各数据库厂商所提供的ErrorCode进行分析要比基于SqlState的方式准确的多。 JdbcTemplate默认情况下采用SQLErrorCodeSQLExceptionTranslator进行SQLException的转译工 作,只有当通过ErrorCode方式无法提供足够信息的时候,才会转而求助于SQLStateSQLExceptionTranslator。
SQLStateSQLExceptionTranslator根据SQLException.getSQLState()所返回的信息进行异常转 译,虽然基于SQLState的方式理论上应该是最通用的,因为SQLState要求符合XOPEN SQLstate 规范或者SQL 99 规范,但各数据库厂商在执行上存在差异, 所以,更多时候,我们偏向于基于ErrorCode的方式。
如果JdbcTemplate默认的SQLErrorCodeSQLExceptionTranslator无法满足当前异常转译的需要,我们可以扩展SQLErrorCodeSQLExceptionTranslator,使他支持等多的情况,这有两种办法,分别是“提供SQLErrorCodeSQLExceptionTranslator的子类”或者“在Classpath的根路径下添加固定的配置文件”, 当然了,在此之前,我们还是先搞清楚为什么要这么做吧!
SQLErrorCodeSQLExceptionTranslator进行异常转译的流程大体上这样的:
Procedure 1.2. SQLErrorCodeSQLExceptionTranslator的异常转译流程
-
SQLErrorCodeSQLExceptionTranslator中定义了如下的自定义异常转译方法:
DataAccessException customTranslate(String task, String sql, SQLException sqlEx)
程序流程首先会检查该自定义异常转译方法是否能够对当前传入的SQLException进行转译,如果可以,则直接返回转译后的 DataAccessException类型; 如果该方法返回null,则表示当前方法无法对传入的SQLException进行转译,程序流程将进入下一步,寻求其他方式进行转译;SQLErrorCodeSQLExceptionTranslator中,该方法直接返回null,所以,我们进入下一步;
-
如果我们应用程序运行于Java6之上,那么,SQLErrorCodeSQLExceptionTranslator会尝试让 SQLExceptionSubclassTranslator来进行异常的转译, 对于使用java6之前的应用来说,这一步基本可以忽略了,了解即可;
-
使用org.springframework.jdbc.support.SQLErrorCodesFactory所加载的SQLErrorCodes进行异常转移,其中,SQLErrorCodesFactory加载SQLErrorCodes的流程为:
-
加载位于spring发布jar包中org/springframework/jdbc/support/sql-error-codes.xml路径下的记载了各个数据库厂商errorCode的配置文件, 提取相应的SQLErrorCodes;
-
如果发现当前应用的Classpath的根路径下存在名称为sql-error-codes.xml的配置文件,则加载该文件内容,并覆盖默认的ErrorCode定义;
-
-
如果基于ErrorCode的异常转译搞不定的话,SQLErrorCodeSQLExceptionTranslator将求助于 SQLStateSQLExceptionTranslator,最后使用基于SQLState的方式进行SQLException到Spring数据访 问异常体系的转译工作。
在以上SQLErrorCodeSQLExceptionTranslator进行异常转译的整个流程中,我们可以在两个点插入自定义的异常转译逻辑:
-
流程的第一步首先检查customTranslate方法是否可以完成转译工 作,SQLErrorCodeSQLExceptionTranslator默认实现返回null,不做自定义的转译工作, 所以,我们可以继承SQLErrorCodeSQLExceptionTranslator以实现它的一个子类,在子类中覆写(override)该方 法,然后使用这个子类替代SQLErrorCodeSQLExceptionTranslator;
-
SQLErrorCodesFactory在为SQLErrorCodeSQLExceptionTranslator加载 ErrorCode配置文件的时候,第二步尝试从Classpath的根路径下加载自定义的名称为sql-error-codes.xml的配置文件以覆 盖默认的定义, 我们可以提供一个名称为sql-error-codes.xml的配置文件,在其中追加自定义的ErrorCode定义;
通过扩展SQLErrorCodeSQLExceptionTranslator以做到自定义异常转译,虽然是最直接有效的方式,却算不上方便。
首先,我们需要定义SQLErrorCodeSQLExceptionTranslator的子类,然后覆写它的customTranslate方法:
public class ToySqlExceptionTranslator extends SQLErrorCodeSQLExceptionTranslator { @Override protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) { if(sqlEx.getErrorCode() == 123456) { String msg = new StringBuffer().append("unexpected data access exception raised when executing ") .append(task) .append(" with SQL>") .append(sql) .toString(); return new UnexpectedDataAccessException(msg,sqlEx); } return null; } }在这里,我们假设当数据库返回的错误码为123456的时候,将抛出UnexpectedDataAccessException类型的异常(或者其他自 定义的DataAccessException实现), 除此之外,我们返回null以保证其他的异常转译依然采用超类SQLErrorCodeSQLExceptionTranslator原来的逻辑进行。
为了能够让自定义的异常转译逻辑生效,我们需要让JdbcTemplate使用我们的ToySqlExceptionTranslator,而不是默认的SQLErrorCodeSQLExceptionTranslator:
DataSource dataSource = ...; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); // set up custom SQLErrorCodeSQLExceptionTranslator SQLErrorCodeSQLExceptionTranslator sqlExTranslator = new ToySqlExceptionTranslator(); sqlExTranslator.setDataSource(dataSource); jdbcTemplate.setExceptionTranslator(sqlExTranslator); // do data access with jdbcTemplate ...至此,通过扩展SQLErrorCodeSQLExceptionTranslator以达到自定义异常转译的目的已经达到,不过,与下面的方式比起来, 具体实践上面则要稍逊一筹啦。
在Classpath的根路径下放置名称为sql-error-codes.xml的配置文件,格式需要与默认的org/springframework/jdbc/support/sql-error-codes.xml文件格式相同。
org/springframework/jdbc/support/sql-error-codes.xml的内容摘录如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <!-- - Default SQL error codes for well-known databases. - Can be overridden by definitions in a "sql-error-codes.xml" file - in the root of the class path. - - If the Database Product Name contains characters that are invalid - to use in the id attribute (like a space) then we need to add a property - named "databaseProductName"/"databaseProductNames" that holds this value. - If this property is present, then it will be used instead of the id for - looking up the error codes based on the current database. --> <beans> <bean id="DB2" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="databaseProductName"> <value>DB2*</value> </property> <property name="badSqlGrammarCodes"> <value>-204,-206,-301,-408</value> </property> <property name="dataAccessResourceFailureCodes"> <value>-904</value> </property> <property name="dataIntegrityViolationCodes"> <value>-803</value> </property> <property name="deadlockLoserCodes"> <value>-911,-913</value> </property> </bean> ... </beans>实际上,它就是一个基本的基于DTD的Spring IoC容器配置文件,只不过,配置的class类型则是固定死的。
该配置文件中,针对每一个数据库类型都提供了一个org.springframework.jdbc.support.SQLErrorCodes 类型的bean定义, 然后根据各个数据库的情况通过相应的setter方法为SQLErrorCodes设置合适的errorCode或者数据库产品别名等。
要扩展异常转译,我们可以根据情况提供Classpath根路径下的sql-error-codes.xml配置文件内容:
-
如果默认的org/springframework/jdbc/support/sql-error-codes.xml配置文件中,缺少 对应你的应用程序所使用的数据库的bean定义配置, 我们直接拷贝现有的某个bean定义到Classpath根路径下的sql-error-codes.xml中,然后修改相应属性值即可:
<bean id="MyDB" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="databaseProductName"> <value>MyDBAlias</value> </property> <property name="badSqlGrammarCodes"> <value>000</value> </property> <property name="dataAccessResourceFailureCodes"> <value>111</value> </property> <property name="dataIntegrityViolationCodes"> <value>222</value> </property> <property name="deadlockLoserCodes"> <value>333</value> </property> </bean>
注意,我们把数据库名称改为了MyDB之类,相应的错误号也做了改变,当然,这毕竟是例子,所有的东西都是伪造的,如果真要提供这样的配置内容,需要根据具体的数据库信息才可进行。 -
如果需要扩展自定义异常转译属于现有的数据库,那么,我们就从org/springframework/jdbc/support /sql-error-codes.xml中拷贝对应应用程序所用数据库的bean定义内容到Classpath根路径下的sql-error- codes.xml中, 只不过,现在我们要追加新的元素:
<bean id="DB2" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="databaseProductName"> <value>DB2*</value> </property> <property name="customTranslations"> <list> <bean class="org.springframework.jdbc.support.CustomSQLErrorCodesTranslation"> <property name="errorCodes"> <value>123456</value> </property> <property name="exceptionClass"> <value>...ToySqlExceptionTranslator</value> </property> </bean> </list> </property> </bean>
只要通过SQLErrorCodes的“customTranslations”属性传入要扩展的自定义异常转译的必要信息即可。 “customTranslations” 为org.springframework.jdbc.support.CustomSQLErrorCodesTranslation类型数组,你可以 通过该属性指定多个CustomSQLErrorCodesTranslation实例。 而CustomSQLErrorCodesTranslation可以简单看成是ErrorCode到异常类型的映射包装类,仅此而已。
我不得不承认的一点就是,之前对JdbcTemplate的实现原理和细节说了那么多,无非是想让你知道,当需要一个类似的轮子的时候,你该如何去 造一个出来。 但是,当有现成的轮子存在的时候,我希望我们去使用这个现成的轮子,而不是耗费人力物力去重造一个,所以,下面主要是告诉你有哪些轮子可用,这些轮子又是 如何使用的。
Spring框架最初是只提供了JdbcTemplate这一个实现,但随着Java版本升级,并且考虑到使用中的便利性等问题,spring在新 发布的版本中又为JdbcTemplate添加了两位兄弟,一个是 org.springframework.jdbc.core.simple.SimpleJdbcTemplate,主要面向Java5提供的一些便 利; 另一个是org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate, 可以在SQL中使用名称代替原先使用的“?”占位符(Placeholder)。 下面,让我们先从JdbcTemplate开始,亲身领略一下spring所提供的数据访问方式的便利和优雅。
JdbcTemplate的初始化很容易,只要通过构造方法传入它所使用的DataSource就可以,如果我们使用Jakarta Commons DBCP,那么,初始化代码看起来是这样的:
BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost/mysql?useUnicode=true&characterEncoding=utf8&failOverReadOnly=false&roundRobinLoadBalance=true"); dataSource.setUsername("user"); dataSource.setPassword("password"); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); // do data accesss with jdbcTemplate当然你也可以通过无参数的构造方法来实例化JdbcTemplate,然后通过setDataSource()来设置所使用的DataSource。
当然,这仅限于编码的方式来初始化JdbcTemplate,不过,如果我们的应用程序使用Spring的IoC容器的话, 那JdbcTemplate的初始化工作当然就可以转移到容器的配置文件中啦:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${jdbc.url}</value> </property> <property name="driverClassName"> <value>${jdbc.driver}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> ... </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource"> <ref bean="dataSource"/> </property> </bean>之后,你想把jdbcTemplate注入那个依赖于它的对象都可以。 不过,这里需要注意的不是JdbcTemplate的配置,而是DataSource的配置,我们使用了自定义的销毁对象的回调方法(destroy- method="close"),以确保应用退出后,数据库连接可以关闭。
好了,JdbcTemplate初始化完成之后,我们就可以大展拳脚了...
该部分仅从整体上对如何使用JdbcTemplate进行基本的数据访问进行介绍,更多JdbcTemplate使用的细节请参照Spring参考文档以及相应的JavaDoc文档。
JdbcTemplate针对数据查询提供了多个重载的模板方法,你可以根据需要选用不同的模板方法。 如果你的查询很简单,仅仅是传入相应SQL或者相关参数,然后取得一个单一的结果,那么你可以选择如下一组便利的模板方法:
-
int queryForInt(String sql)
-
int queryForInt(String sql, Object[] args)
-
long queryForLong(String sql)
-
long queryForLong(String sql, Object[] args)
-
Object queryForObject(String sql, Class requiredType)
-
Object queryForObject(String sql, Object[] args, Class requiredType)
-
Map queryForMap(String sql)
-
Map queryForMap(String sql, Object[] args)
int age = jdbcTemplate.queryForInt("select age from customer where customerId=?",new Object[]{new Integer(100)}); ... long interval = jdbcTemplate.queryForLong("select count(customerId) from customer"); ... String customerName = jdbcTemplate.queryForString("select username from customer where customerId=110"); ... Map singleCustomer = jdbcTemplate.queryForMap("select * from customer limit 1"); ...queryForMap方法与其他方法不同之处在于,它的查询结果以java.util.Map的形式返回,Map的key对应所查询表的列名,Map的 value当然就是对应key所在列的值啦。 当然了,你也看到了,这组模板方法主要用于单一结果的查询,使用的时候也请确保你的SQL查询所返回的结果是单一的,否则,JdbcTemplate将抛 出org.springframework.dao.IncorrectResultSizeDataAccessException异常。
如果查询的结果将返回多行,而你又不在乎他们是否拥有较强的类型约束,那么,以下模板方法可以帮助你:
-
List queryForList(String sql)
-
List queryForList(String sql, Object[] args)
好啦,如果这些还不足以满足你的查询需要,那么我们就更进一步,使用相应的Callback接口对查询结果的返回进行定制吧!
用于查询的回调接口定义主要有以下三种:
-
org.springframework.jdbc.core.ResultSetExtractor. 基本上属于JdbcTemplate内部使用的Callback接口,相对于下面两个Callback接口来说,ResultSetExtractor拥有更多的控制权,因为使用它,你需要自行处理ResultSet:
public interface ResultSetExtractor { Object extractData(ResultSet rs) throws SQLException, DataAccessException; }
在直接处理完ResultSet之后,你可以将处理后的结果以任何你想要的形式包装后返回。 -
org.springframework.jdbc.core.RowCallbackHandler. RowCallbackHandler相对于ResultSetExtractor来说,仅仅关注单行结果的处理,处理后的结果可以根据需要存放到当前 RowCallbackHandler对象内或者使用JdbcTemplate的程序上下文中,当然,这个完全是看个人爱好了。 RowCallbackHandler的定义如下:
public interface RowCallbackHandler { void processRow(ResultSet rs) throws SQLException; }
-
org.springframework.jdbc.core.RowMapper. ResultSetExtractor的精简版,功能类似于RowCallbackHandler,也只关注处理单行的结果,不过,处理后的结果会由ResultSetExtractor实现类进行组合。 RowMapper的接口定义如下:
public interface RowMapper { Object mapRow(ResultSet rs, int rowNum) throws SQLException; }
数据库表customer中存在多行信息,对该表查询后,我们需要将每一行的顾客信息都映射到域对象Customer中,并以java.util.List的形式返回所有的查询结果。
List customerList = (List)jdbcTemplate.query("select * from customer", new ResultSetExtractor(){ public Object extractData(ResultSet rs) throws SQLException,DataAccessException { List customers = new ArrayList(); while(rs.next()) { Customer customer = new Customer(); customer.setFirstName(rs.getString(1)); customer.setLastName(rs.getString(2)); ... customers.add(customer); } return customers; }});
List customerList = jdbcTemplate.query("select * from customer", new RowMapper(){ public Object mapRow(ResultSet rs, int rowNumber) throws SQLException { Customer customer = new Customer(); customer.setFirstName(rs.getString(1)); customer.setLastName(rs.getString(2)); ... return customer; }});
final List customerList = new ArrayList(); jdbcTemplate.query("select * from customer", new RowCallbackHandler(){ public void processRow(ResultSet rs) throws SQLException { Customer customer = new Customer(); customer.setFirstName(rs.getString(1)); customer.setLastName(rs.getString(2)); ... customerList.add(customer); }});如果你没有发现最大的差异在哪里,那么容我细表:
-
使用三种Callback接口作为参数的query方法的返回值不同:
-
以ResultSetExtractor作为方法参数的query方法返回Object型结果,要使用查询结果,我们需要对其进行强制转型;
-
以RowMapper接口作为方法参数的query方法直接返回List型的结果;
-
以RowCallbackHandler作为方法参数的query方法,返回值为void;
-
-
使用ResultSetExtractor作为Callback接口处理查询结果,我们需要自己声明集合类,自己遍历ResultSet, 自己根据每行数据组装Customer对象,自己将组装后的Customer对象添加到集合类中,方法最终只负责将组装完成的集合返回;
-
使用RowMapper比直接使用ResultSetExtractor要方便的多,只负责处理单行结果就行,现在,我们只需要将单行的结 果组装后返回就行,剩下的工作,全部都是JdbcTemplate内部的事情了。 实际上,JdbcTemplae内部会使用一个ResultSetExtractor实现类来做其余的工作,毕竟,该做的工作还得有人做不是?!
JdbcTemplae内部使用的这个ResultSetExtractor实现类为 org.springframework.jdbc.core.RowMapperResultSetExtractor, 它内部持有一个RowMapper实例的引用,当处理结果集的时候,会将单行数据的处理委派给其所持有的RowMapper实例,而其余工作它负责:
public Object extractData(ResultSet rs) throws SQLException { List results = (this.rowsExpected > 0 ? new ArrayList(this.rowsExpected) : new ArrayList()); int rowNum = 0; while (rs.next()) { results.add(this.rowMapper.mapRow(rs, rowNum++)); } return results; }
这下应该清楚为啥RowMapper为啥就处理单行结果就能完成ResultSetExtractor颇费周折的工作了吧?! -
RowCallbackHandler虽然与RowMapper同是处理单行数据,不过,除了要处理单行结果,它还得负责最终结果的组装和 获取工作,在这里我们是使用当前上下文声明的List取得最终查询结果, 不过,我们也可以单独声明一个RowCallbackHandler实现类,在其中声明相应的集合类,这样,我们可以通过该 RowCallbackHandler实现类取得最终查询结果:
public class GenericRowCallbackHandler implements RowCallbackHandler { private List collections = new ArrayList(); public void processRow(ResultSet rs) throws SQLException { Customer customer = new Customer(); customer.setFirstName(rs.getString(1)); customer.setLastName(rs.getString(2)); ... collections.add(customer); } public List getResults() { return collections; } } GenericRowCallbackHandler handler = new GenericRowCallbackHandler(); jdbcTemplate.query("select * from customer",handler()); List customerList = handler.getResults();
该使用方式是明了了,不过GenericRowCallbackHandler重用性不佳。RowCallbackHandler因为也是处理单行数据,所以,总得有人来做遍历ResultSet的工作,这个人其实也是一个 ResultSetExtractor实现类, 它是JdbcTemplate一个内部静态类,名为RowCallbackHandlerResultSetExtractor,一看它的定义你就知道奥 秘之所在了:
private static class RowCallbackHandlerResultSetExtractor implements ResultSetExtractor { private final RowCallbackHandler rch; public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch) { this.rch = rch; } public Object extractData(ResultSet rs) throws SQLException { while (rs.next()) { this.rch.processRow(rs); } return null; } }
对于使用JdbcTemplate进行查询,基本就这些内容了,当然,如果你非要使用基于StatementCallback之类更底层的 execute方法的话,那就是你个人说了算啦。 不过,要想知道JdbcTemplate中有关查询相关模板方法的更多信息,在实际使用中参考JdbcTemplate的javadoc就可以,当然,有 IDE就更便捷了。
相对于查询来说,使用JdbcTemplate进行数据更新就没有那么多说道了。不管你是要对数据库进行数据插入,还是更新甚至删除,你都可以通过JdbcTemplate所提供的一组重载的update()模板方法进行, 这些update方法包括:
-
int update(String sql)
-
int update(String sql, Object[] args)
-
int update(String sql, Object[] args, int[] argTypes)
// insert jdbcTemplate.update("insert into customer(customerName,age,...) values("darren","28",...)"); // update int affectedRows = jdbcTemplate.update("update customer set customerName='daniel',age=36 where customerId=101"); // or int affectedRows = jdbcTemplate.update("update customer set customerName=?,age=? where customerId=?", new Object[]{"Daniel",new Integer(36),new Integer(101)}); // delete int deletedRowCount = jdbcTemplate.update("delete from customer where customerId between 1 and 100");通常情况下,接受简单的SQL以及相关参数的update方法就能够满足数据更新的需要了,不过,如果你觉得有必要对更新操作有更多的控制权,那么,你可 以使用与PreparedStatement相关的Callback接口, 这包括使用PreparedStatementCreator创建PreparedStatement,使用 PreparedStatementSetter对相关占位符进行设置等。 同样的对一条记录进行更新,使用Callback接口作为参数的update方法的数据访问代码看起来如下:
// int update(String sql, PreparedStatementSetter pss) int affectedRows = jdbcTemplate.update("update customer set customerName=?,age=? where customerId=?", new PreparedStatementSetter(){ public void setValues(PreparedStatement ps) throws SQLException { ps.setString(1,"Daniel"); ps.setInt(2,36); ps.setInt(3,101); }}); // int update(PreparedStatementCreator psc) int affectedRows = jdbcTemplate.update(new PreparedStatementCreator(){ public PreparedStatement createPreparedStatement(Connection con) throws SQLException { PreparedStatement ps = con.prepareStatement("update customer set customerName=?,age=? where customerId=?"); ps.setString(1,"Daniel"); ps.setInt(2,36); ps.setInt(3,101); return ps; }});使用update方法进行数据更新可以获得最终更新操作所影响的记录数目,而且,如果不单单指定一个SQL作为参数的话,JdbcTemplate内部会构造相应的PreparedStatement进行实际的更新操作。
不过,除了使用update方法,你还可以通过“只接受SQL语句作为参数的execute()方法”进行数据更新,该方法没有返回值,所以,更加适合那种不需要返回值的操作,比如删除表,创建表等操作:
jdbcTemplate.execute("create table customer (...)"); // or jdbcTemplate.execute("drop table customer");至于其他重载的execute()方法,相对来说过于贴近JDBC API了,通常情况下,我们没有必要使用,某些时候为了集成遗留系统中某些基于Jdbc的数据访问代码倒是有可能需要求助于这些execute方法。
对于更新同一数据表的多笔更新操作,我们可以使用Jdbc的批量更新(Batch Update)功能对这些更新操作进行统一提交执行,以避免每一笔更新都单独执行,这样可以大大提高更新的执行效率。
JdbcTemplate提供了两个重载的batchUpdate()方法以支持批量更新操作:
-
int[] batchUpdate(String[] sql)
-
int[] batchUpdate(String sql, BatchPreparedStatementSetter pss)
假设我们要将传入的新追加顾客列表的信息追加到数据库,那么我们就可以使用JdbcTemplate的批量更新支持:
public int[] insertNewCustomers(final List customers) { jdbcTemplate.batchUpdate("insert into customer value(?,?,...)", new BatchPreparedStatementSetter(){ public int getBatchSize() { return customers.size(); } public void setValues(PreparedStatement ps, int i) throws SQLException { Customer customer = (Customer)customers.get(i); ps.setString(1,customer.getFirstName()); ps.setString(2,customer.getLastName()); ... }}); }因为我们的更新语句中牵扯参数,所以,我们使用BatchPreparedStatementSetter回调接口来对批量更新中每次更新所需要的参数进行设置。 BatchPreparedStatementSetter有两个方法需要我们实现:
-
int getBatchSize(). 返回批量更新的数目,因为我们要对通过List传入的所有顾客信息进行更新,所以,当前批量更新的数目就是当前List中所有的顾客数目;
-
void setValues(PreparedStatement ps, int i) . 设置具体的更新数据,其中第二个int型的参数对应的是每笔更新的索引,我们就是根据这个索引从customers列表中取得相应的信息进行设置的。
存储过程属于定义于数据库服务器端的计算单元,对于牵扯多表数据而单单使用SQL无法完成的计算,我们可以通过在数据库服务器端编写并部署存储过程 的方式来实现, 相对于将这些计算逻辑转移到客户端进行的好处在于,使用存储过程可以避免像客户端计算那样在网路间来回传送数据导致的性能损失,因为存储过程的所有计算全 部在服务器端完成, 所以,如果计算牵扯多个数据表,大量的数据查询和更新,那么,使用存储过程代替客户端计算是比较合适的做法。
存储过程(Stored Procedure)不属于核心SQL标准的一部分,所以,并非所有关系数据库都提供对存储过程的支持,但存储过程在许多企业应用中具有重要地位,所以,Jdbc标准也通过提供CallableStatement支持对现有存储过程的调用。
假设我们有以下的存储过程定义(MySQL的定义语法):
CREATE PROCEDURE CountTable(IN tableName varchar(1000),OUT sqlStr varchar(1000) , INOUT v INT) BEGIN set @flag = v; set @sql = CONCAT('select count(*) into @res from ' , tableName , ' where ACTIVE_FLAG=?'); PREPARE stmt FROM @sql; EXECUTE stmt using @flag; DEALLOCATE PREPARE stmt; set v = @res; set sqlStr = @sql; END该存储过程定义了三个Parameter:
-
tableName为IN参数,字符串类型;
-
sqlStr为OUT参数,也是字符串类型;
-
v为INOUT参数,INT类型;
Connection conn = null; CallableStatement stat = null; try{ conn = dataSource.getConnection(); stat = conn.prepareCall("call CountTable(?,?,?)"); stat.setString(1, "TableName"); stat.setInt(3, 1); stat.registerOutParameter(2, Types.VARCHAR); stat.registerOutParameter(3, Types.INTEGER); stat.execute(); String sql = stat.getString(2); int count = stat.getInt(3); System.out.println("SQL:"+sql); System.out.println("Record count:"+count); }catch(Exception dx){ dx.printStackTrace(); }finally{ if(null!=stat) try{stat.close();}catch(Exception dx){} if(null!=conn) try{conn.close();}catch(Exception dx){} }
JdbcTemplate同样对存储过程的调用进行了模板化处理,对于同一存储过程,我们来看使用JdbcTemplate后是是怎么一个样子:
Object result = jdbcTemplate.execute("call CountTable(?,?,?)", new CallableStatementCallback(){ public Object doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException { // declare and set IN/OUT paramters cs.setString(1, "tableName"); cs.setInt(3, 1); cs.registerOutParameter(2, Types.VARCHAR); cs.registerOutParameter(3, Types.INTEGER); // execute Call cs.execute(); // extract result and return Map result = new HashMap(); result.put("SQL", cs.getString(2)); result.put("COUNT", cs.getInt(3)); return result; }});我们直接使用CallableStatementCallback回调接口所暴露的CallableStatement对象句柄进行调用操作,而无需关心CallableStatement以及Connection等资源的管理问题。
或者你可以把CallableStatementCallback的部分职能划分出去,一部分由CallableStatementCreator这个Callback接口分担:
Object result = jdbcTemplate.execute(new CallableStatementCreator(){ public CallableStatement createCallableStatement(Connection con) throws SQLException { CallableStatement cs = con.prepareCall("call CountTable(?,?,?)"); cs.setString(1, "tableName"); cs.setInt(3, 1); cs.registerOutParameter(2, Types.VARCHAR); cs.registerOutParameter(3, Types.INTEGER); return cs; }}, new CallableStatementCallback(){ public Object doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException { cs.execute(); // extract result and return Map result = new HashMap(); result.put("SQL", cs.getString(2)); result.put("COUNT", cs.getInt(3)); return result; }});
除了以上两种调用存储过程的方法,你还可以JdbcTemplate提供的另一个调用存储过程的模板方法:
Map call(CallableStatementCreator csc, List declaredParameters)该模板方法主要好处是可以通过List指定存储过程的参数列表,之后,JdbcTemplate会根据指定的参数列表所提供的参数信息为你组装调用结果, 并以Map形式返回。 declaredParameters参数列表中的元素需要为org.springframework.jdbc.core.SqlParameter类 型或者相关子类,声明顺序和参数类型要与实际存储过程定义的参数顺序和类型相同。
下面是使用call方法调用我们的存储过程的实例代码:
List<SqlParameter> parameters = new ArrayList<SqlParameter>(); parameters.add(new SqlParameter(Types.VARCHAR)); parameters.add(new SqlOutParameter("SQL",Types.VARCHAR)); parameters.add(new SqlInOutParameter("COUNT",Types.INTEGER)); Map result = jdbcTemplate.call(new CallableStatementCreator(){ public CallableStatement createCallableStatement(Connection con) throws SQLException { CallableStatement cs = con.prepareCall("call CountTable(?,?,?)"); cs.setString(1, "tableName"); cs.setInt(3, 1); cs.registerOutParameter(2, Types.VARCHAR); cs.registerOutParameter(3, Types.INTEGER); return cs; }}, parameters); System.out.println(result.get("SQL")); System.out.println(result.get("COUNT"));
使用JdbcTemplate的存储过程调用方法,我们不用关注资源释放之类的问题,仅关注相关参数和结果的处理即可。 当然,虽然可以省却资源管理的烦恼,但使用相关回调接口使得使用JdbcTemplate进行存储过程调用并不是那么令人赏心悦目,如果你不满意此种繁 琐,那么没关系,稍后为您介绍Spring中另一种存储过程调用方式。
如果要为关系数据库新增数据的话,新增数据的主键生成一直是一个需要关注的问题。 对于数据的主键生成的位置来说,你通常有两种选择,或者在数据库服务器端,使用不同的数据库厂商提供的主键生成策略支持;或者直接在应用程序的客户端根据 某些算法生成需要的数据主键。 对于前者来说,虽然可以充分利用数据库的特性以及优化措施,但可移植性比较差,而且某些情况下可能造成数据库过多负担;采用客户端的主键生成策略,可以分 担服务器的负担,而且,主键的生成策略可以根据情况进行调整,灵活性很好,性能也可能随着系统架构的不同而有所提升。
某些应用程序在设计的时候,出于主键生成策略的可移植性或者性能方面的考虑,会在应用程序客户端采用某种主键生成的抽象策略,以统一的方式来进行主 键生成的管理。 大部分情况下,递增的主键生成策略是我们在这种情况下使用最多的主键生成策略。我想,在次之前,你我或多或少都做过同样的事情,起码,笔者经历的FX项目 就采用这种类似的主键生成策略。 现在,spring对递增的主键生成策略进行了适当的抽象,针对不同的关系数据库给出了相应的主键生成实现类,以帮助我们统一基于递增策略的主键生成。
Spring的org.springframework.jdbc.support.incrementer包下是整个针对递增主键生成策略的相关 接口定义和实现类, org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer 是这整个体系的顶层接口定义:
public interface DataFieldMaxValueIncrementer { /** * Increment the data store field's max value as int. * @return int next data store value such as <b>max + 1</b> * @throws org.springframework.dao.DataAccessException in case of errors */ int nextIntValue() throws DataAccessException; /** * Increment the data store field's max value as long. * @return int next data store value such as <b>max + 1</b> * @throws org.springframework.dao.DataAccessException in case of errors */ long nextLongValue() throws DataAccessException; /** * Increment the data store field's max value as String. * @return next data store value such as <b>max + 1</b> * @throws org.springframework.dao.DataAccessException in case of errors */ String nextStringValue() throws DataAccessException; }根据不同数据库对递增主键生成的支持,DataFieldMaxValueIncrementer的相关实现类可以分为两类:
-
基于独立主键表的DataFieldMaxValueIncrementer. 基于独立主键表的DataFieldMaxValueIncrementer依赖于为每一个数据表单独定义的主键表, 主键表中定义的主键可以根据需要获取并递增,并且可以设置每次获取的CacheSize以减少过多的访问数据库资源,Spring为HSQLDB和 MySQL数据库提供的就是这种策略的DataFieldMaxValueIncrementer实现。
-
基于数据库Sequence的DataFieldMaxValueIncrementer. 像DB2或者Oracle等数据库,数据库本身支持基于Sequence的主键生成,所以,Spring在数据库本身Sequence的基础上,为 DB2,Oracle和PostgreSQL等支持Sequence的数据库提供了基于Sequence的 DataFieldMaxValueIncrementer实现类。
HsqlMaxValueIncrementer和MySQLMaxValueIncrementer继承自 AbstractDataFieldMaxValueIncrementer,属于依赖主键定义表的 DataFieldMaxValueIncrementer实现, 要使用这两种DataFieldMaxValueIncrementer实现,我们需要为相应的表定义对应的主键表,以保存相应的主键值。
还记得我们在第一章提到的FX News吗?假设我们采用如下表定义来保存各种新闻内容:
CREATE TABLE fx_news ( news_id bigint(20) NOT NULL, new_title varchar(25) NOT NULL, new_body text NOT NULL, PRIMARY KEY(news_id) )news_id为该表的主键,要使用MySQLMaxValueIncrementer(或者HsqlMaxValueIncrementer)每次插入数据的时候为该字段生成主键值, 我们还需要定义对应fx_news表的主键表,用来计算并保持当前主键值:
CREATE TABLE fx_news_key ( value bigint(20) NOT NULL default 0, PRIMARY KEY(value) ) engine=MYISAM; insert into fx_news_key values(0);注意,为了减少事务开销,我们将fx_news_key主键表的引擎设置为MYISAM,而不是InnoDB。
有了这些,我们就可以在应用程序中使用MySQLMaxValueIncrementer生成递增主键并向关系数据库插入新增数据了:
DataSource dataSource = ...; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); DataFieldMaxValueIncrementer incrementer = new MySQLMaxValueIncrementer(dataSource,"fx_news_key","value"); ((MySQLMaxValueIncrementer)incrementer).setCacheSize(5); jdbcTemplate.update("insert into fx_news(news_id,news_title,news_body) values(?,?,?)", new Object[]{incrementer.nextLongValue(),"title","body"});你需要为MySQLMaxValueIncrementer提供一个DataSource以及对应主键表的表名和相应的列,如果需要,你还可以指定 CacheSize,以便每次取得多个值在本地缓存,从而减少数据库访问次数, 可以通过设置cacheSize进行本地的值缓存是MySQLMaxValueIncrementer和HsqlMaxValueIncrementer 比较实用的一个特色。
不过通常情况下,通过Spring的IoC容器来配置相应的DataFieldMaxValueIncrementer要方便的多,这样,整个应用 中需要递增主键生成支持的类都可以很容易的获取DataFieldMaxValueIncrementer相应实现类的注入:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${jdbc.url}</value> </property> <property name="driverClassName"> <value>${jdbc.driver}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> </bean> <bean id="incrementer" class="org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer"> <property name="dataSource" ref="dataSource"/> <property name="incrementerName" value="fx_news_key"/> <property name="columnName" value="value"/> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="djNewsPersister" class="...DowJonesNewsPersister"> <property name="incrementer" ref="incrementer"/> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean>DowJonesNewsPersister每次向数据新追加新闻数据的时候,就可以使用为其注入的incrementer递增主键了。
使用IoC容器来管理相应的DataFieldMaxValueIncrementer,可以将系统中所有的 DataFieldMaxValueIncrementer实例集中到一个配置模块中,以便于管理和使用。 不过,使用容器管理的DataFieldMaxValueIncrementer可能需要注意一个问题,那就是,容器中的各个 DataFieldMaxValueIncrementer虽然在系统中可以共享, 但即使是在系统不使用的情况下,相应的实例也不会释放,除非系统推出,所以,对于系统资源紧张的应用来说,在合适的时机根据需要实例化相应的 DataFieldMaxValueIncrementer来使用也不失为合适的方式。
对于提供了Sequence支持的数据库来说,DataFieldMaxValueIncrementer实现体系专门在 AbstractDataFieldMaxValueIncrementer的基础上开辟了一个面向基于Sequence的 DataFieldMaxValueIncrementer实现分支, 以AbstractSequenceMaxValueIncrementer作为这个分支的统一超类。Spring为支持Sequence的 DB2,Oracle和PostgreSQL数据库提供了相应的DataFieldMaxValueIncrementer实现类:
-
DB2SequenceMaxValueIncrementer
-
OracleSequenceMaxValueIncrementer
-
PostgreSQLSequenceMaxValueIncrementer
CREATE SEQUENCE fx_news_seq NCREMENT BY 1 START WITH 1 NOMAXVALUE NOCYCLE NOCACHE;之后,我们只要在构造OracleSequenceMaxValueIncrementer的时候,告知对应的Sequence名称即可:
DataSource dataSource = ...; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); DataFieldMaxValueIncrementer incrementer = new OracleSequenceMaxValueIncrementer(dataSource,"fx_news_seq"); jdbcTemplate.update("insert into fx_news(news_id,news_title,news_body) values(?,?,?)", new Object[]{incrementer.nextLongValue(),"title","body"});如果想每次取得一批数据的话,你需要在Sequence的定义中指定,而无法在客户端调用的时候决定是每次取一个数据还是一批数据:
CREATE SEQUENCE fx_news_seq NCREMENT BY 1 START WITH 1 NOMAXVALUE NOCYCLE CACHE 5;当然啦,如果应用程序构建于Spring的IoC容器之上,从“基于主键表的DataFieldMaxValueIncrementer实现类”变为“基于Sequence的DataFieldMaxValueIncrementer实现类”仅仅是简单的配置更改而已:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> ... </bean> <bean id="incrementer" class="org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer"> <property name="dataSource" ref="dataSource"/> <property name="incrementerName" value="fx_news_seq"/> </bean> ... <bean id="djNewsPersister" class="...DowJonesNewsPersister"> <property name="incrementer" ref="incrementer"/> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean>其他的bean定义和使用DataFieldMaxValueIncrementer的类逻辑基本不用动。
总之,如果需要递增的主键生成策略,根据应用程序使用的数据库选用相应的DataFieldMaxValueIncrementer实现类即可,如 果Spring框架没有提供当前应用程序所用数据库的DataFieldMaxValueIncrementer实现类,那不妨在 AbstractDataFieldMaxValueIncrementer或者 AbstractSequenceMaxValueIncrementer的基础上扩展一个啦!
1.2.1.3.1.4. Spring中的Lob[5]类型处理
Lob是Large OBject的简称,指得是数据库中能够存取大量数据的数据类型[6]。 按照存放具体存放的数据形式,Lob类型通常分为BLOB和CLOB两种类型[7]:
-
BLOB指的是Binary Large OBject,主要用于存放数据量比较大的二进制类型数据,比如比较大的图像文件,Word文档之类二进制文件。
-
CLOB指得是Character Large Object,主要用于存放数据量比较大的文本类型数据。
除了Oracle,其他数据库在进行LOB字段的更新和读取的时候,与通常的字段类型没有太多差别,大都是通过PreparedStatement 的相应方法设置LOB的值,然后通过ResultSet的相应方法获取结果集中的LOB数据。 假设,我们有如下的表定义用于存放图像文件相关数据(以MySQL方式定义):
CREATE TABLE images ( id int(11) NOT NULL, filename varchar(200) NOT NULL, entity blob NOT NULL, description text NULL, PRIMARY KEY(id) )只要不是Oracle数据库,我们就可以按照通常的JDBC操作方式对blog类型进行更新和读取(忽略异常处理):
// --- save file data into blob as binary stream --- File imageFile = new File("snow_image.jpg"); InputStream ins = new FileInputStream(imageFile); Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("insert into images(id,filename,entity,description) values(?,?,?,?)"); ps.setInt(1, 1); ps.setString(2, "snow_image.jpg"); ps.setBinaryStream(3, ins,(int)imageFile.length()); ps.setString(4, "nothing to say"); ps.executeUpdate(); ps.close(); con.close(); IOUtils.closeQuietly(ins); ... // --- read data as binary stream --- File imageFile = new File("snow_image_copy.jpg"); InputStream ins = null; Connection con = dataSource.getConnection(); Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery("select entity from images where id=1"); while(rs.next()) { ins = rs.getBinaryStream(1); } rs.close(); stmt.close(); con.close(); OutputStream ous = new FileOutputStream(imageFile); IOUtils.write(IOUtils.toByteArray(ins), ous); IOUtils.closeQuietly(ins); IOUtils.closeQuietly(ous);在这里,我们直接使用PreparedStatement的setBinaryStream()方法对blob类型数据进行存储,使用ResultSet 的getBinaryStream()方法对blob数据进行读取(你也可以使用针对Object和byte[]类型的 PreparedStatement的setXXX()方法或者ResultSet的getXXX()方法对BLOB数据进行存储,更多信息请参考 JDBC文档)。
可是一旦数据库变为Oracle,那就来麻烦了,你只能通过Oracle驱动程序提供的oracle.sql.BLOB或者 oracle.sql.CLOB实现类对LOB数据进行操作,而无法通过标准JDBC API进行。 在Oracle 9i中,如果我们要同样的插入一笔数据,那么代码看起来像这样:
Connection con = ...; Statement stmt = con.createStatement(); // 1. 要插入一笔BLOB数据,需要先插入empty blob以占位 stmt.executeUpdate("insert into images(id,filename,entity,description) values(1,'snow_image.jpg',empty_blob(),'no desc')"); // 2. 取回对应记录的BLOB的locator,然后通过locator写入数据 ResultSet rs = stmt.executeQuery("select entity from images where id=1"); rs.next(); BLOB blob = ((OracleResultSet)rs).getBLOB(1); File imageFile = new File("snow_image.jpg"); InputStream ins = new FileInputStream(imageFile); OutputStream ous = blob.getBinaryOutputStream(); IOUtils.write(IOUtils.toByteArray(ins), ous); IOUtils.closeQuietly(ins); IOUtils.closeQuietly(ous); rs.close(); stmt.close(); con.close();对于查询来说,也要通过oracle.sql.BLOB或者oracle.sql.CLOB实现类进行:
Connection con = ...; Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery ("select entity from images where id=1"); rs.next(); BLOB blob = ((OracleResultSet)rs).getBLOB(1); //使用blob.getBinaryStream()或者getBytes()方法处理结果即可 rs.close(); stmt.close(); con.close();
鉴于对LOB数据处理方式的不一致性,spring在org.springframework.jdbc.support.lob包下面提出了一套 LOB数据处理类,用于屏蔽各数据库驱动在处理LOB数据方式上的差异性。 org.springframework.jdbc.support.lob.LobHandler接口是spring框架得以屏蔽LOB数据处理差异性 的核心,它只定义了对BLOB和CLOB数据的操作接口,而具体的实现则下放给具体的实现类来做。 你可以通过LobHandler提供的各种BLOB和CLOB数据访问方法对LOB以需要的方式进行读取:
public interface LobHandler { byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException; byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException; InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException; InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; String getClobAsString(ResultSet rs, String columnName) throws SQLException; String getClobAsString(ResultSet rs, int columnIndex) throws SQLException; InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException; InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException; Reader getClobAsCharacterStream(ResultSet rs, String columnName) throws SQLException; Reader getClobAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; LobCreator getLobCreator(); }LobHandler除了作为LOB数据的访问接口,它还有一个角色,那就是它还是 org.springframework.jdbc.support.lob.LobCreator的生产工厂,从LobHandler定义的最后一行你 应该看得出来。 LobCreator的职责主要在于LOB数据的创建,它让你能够以统一的方式创建LOB数据,我们将在插入或者更新LOB数据的时候使用它,不过在此之 前,我们先把LobCreator放一边,继续关注LobHandler。
LobHandler的继承关系如下图所示:
整个层次很简单,在LobHandler之下实现一个AbstractLobHandler抽象类以简化子类的实现,这个抽象类的逻辑很简单,就是将LobHandler中位于一组的重载方法其中一个的逻辑委托给另一个,比如:
public abstract class AbstractLobHandler implements LobHandler { public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException { return getBlobAsBytes(rs, rs.findColumn(columnName)); } ... }所以,我们应该更多的关注OracleLobHandler和DefaultLobHandler这两个具体实现类:
-
org.springframework.jdbc.support.lob.OracleLobHandler是专门针对Oracle数 据库的LobHandler实现, 通过Oracle数据库驱动的原始API进行LOB数据的操作。因为要用到驱动程序的原始API,所以,在使用OracleLobHandler的时候, 我们需要根据情况为它提供相应的NativeJdbcExtractor实现。 比如,如果我们通过Commons DBCP数据库缓冲池管理Oracle数据库连接的话,那么,在使用OracleLobHandler进行Lob数据处理之前,需要为其设置 CommonsDbcpNativeJdbcExtractor:
OracleLobHandler lobHandler = new OracleLobHandler(); lobHandler.setNativeJdbcExtractor(new CommonsDbcpNativeJdbcExtractor()); // lobHandler ready to use
-
除了Oracle之外的大多数数据库可以使用DefaultLobHandler作为他们的LobHandler实现用来操作LOB数据。 DefaultLobHandler主要通过标准的JDBC API来创建和访问LOB数据。
使用JdbcTemplate对LOB数据进行操作,我们通常使用 org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback 作为Callback接口, 该类构造的时候接受一个LobHandler作为构造方法参数。
同样向images表插入一条数据,使用JdbcTemplate和AbstractLobCreatingPreparedStatementCallback之后的数据访问代码如下:
final File imageFile = new File("snow_image.jpg"); final InputStream ins = new FileInputStream(imageFile); LobHandler lobHandler = new DefaultLobHandler(); jdbcTemplate.execute("insert into images(id,filename,entity,description) values(?,?,?,?)", new AbstractLobCreatingPreparedStatementCallback(lobHandler){ @Override protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException, DataAccessException { ps.setInt(1, 2); ps.setString(2, "snow_image.jpg"); lobCreator.setBlobAsBinaryStream(ps, 3, ins, (int)imageFile.length()); ps.setString(4, "nothing to say"); }}); IOUtils.closeQuietly(ins);查询数据的访问代码则看起来如下:
final LobHandler lobHandler = new DefaultLobHandler(); InputStream ins = (InputStream)jdbcTemplate.queryForObject("select entity from images where id=1", new RowMapper(){ public Object mapRow(ResultSet rs, int row) throws SQLException { return lobHandler.getBlobAsBinaryStream(rs, 1); }}); // write lob data into file File imageFile = new File("snow_image_copy.jpg"); OutputStream ous = new FileOutputStream(imageFile); IOUtils.write(IOUtils.toByteArray(ins), ous); IOUtils.closeQuietly(ins); IOUtils.closeQuietly(ous);对于查询来说,如果是特定于LOB结果的处理的话,通常使用org.springframework.jdbc.core.support.AbstractLobStreamingResultSetExtractor作为结果集的处理Callback接口:
final OutputStream ous = new FileOutputStream(imageFile); jdbcTemplate.query("select entity from images where id=1", new AbstractLobStreamingResultSetExtractor(){ @Override protected void streamData(ResultSet rs) throws SQLException, IOException, DataAccessException { InputStream ins = lobHandler.getBlobAsBinaryStream(rs, 1); IOUtils.write(IOUtils.toByteArray(ins), ous); IOUtils.closeQuietly(ins); }}); IOUtils.closeQuietly(ous);现在要对读取后的数据作何处理,我们直接在streamData方法中指定就可以了。
如果应用程序使用Spring的IoC容器的话,我们可以将LobHandler的定义追加的容器的配置文件中,如果因为数据库的变动需要变换 LobHandler具体实现类的话, 那也仅仅是简单的配置变更,所以,笔者强烈建议通过Spring的IoC容器管理整个应用的配置和运行。
对于每次调用都需要动态指定查询或者更新参数的SQL来说,通常或者说自始至终,我们都是通过“?”作为SQL参数的占位符的,有了NamedParameterJdbcTemplate的支持之后, 我们就可以通过容易记忆或者更加有语义的符号来作为SQL中的参数占位符,如果你觉得使用符号作为占位符更好,那你就把“?”形式的占位符抛入历史的尘埃中去; 如果你觉得两种占位符方式都还可以,那你也算有了更多的选择。
让我们先看一下同一条SQL语句,使用“?”作为占位符和使用命名的参数符号作为占位符前后有何差别:
前 select count(*) from images where filename=? 后 select count(*) from images where filename=:filename“:filename”就是命名的参数符号(如果你用过Ruby,是不是会觉得很熟悉那?),通过NamedParameterJdbcTemplate,我们就可以执行这种使用命名参数符号的SQL语句:
DataSource dataSource = ...; NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); SqlParameterSource parameterSource = new MapSqlParameterSource("filename","snow_image.jpg"); int count = npJdbcTemplate.queryForInt("select count(*) from images where filename=:filename", parameterSource); // process count varible if necessary使用NamedParameterJdbcTemplate的时候,我们现在不是通过Object[]数组的形式为相应占位符提供参数值,而是通过 org.springframework.jdbc.core.namedparam.SqlParameterSource接口, 该接口定义有两个实现类,分别是 org.springframework.jdbc.core.namedparam.MapSqlParameterSource和 org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource, 在上例中,我们已经使用了MapSqlParameterSource,它持有一个Map实例,所有的命名参数符号以及他们的值都是存入这个持有的Map 实例中,所以,如果SQL中不止一个命名参数符号的话,我们也可以通过MapSqlParameterSource的 addValue(String,Object)添加多个命名参数符号及相关值:
if SQL is : select count(*) from images where filename=:filename and description=:description SqlParameterSource parameterSource = new MapSqlParameterSource("filename","snow_image.jpg"); parameterSource = parameterSource.addValue("description","something"); npJdbcTemplate.queryForInt(SQL, parameterSource);因为MapSqlParameterSource实际上就是对Map的一个封装,所以,NamedParameterJdbcTemplate也提供了使 用Map作为方法参数的重载的模板方法,我们也可以直接使用Map来替代相应的SqlParameterSource实例(不仅仅 MapSqlParameterSource):
DataSource dataSource = ...; NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); Map parameters = new HashMap(); parameters.put("filename","snow_image.jpg"); parameters.put("description","something"); int count = npJdbcTemplate.queryForInt(SQL, parameters); // process count varible if necessary
SqlParameterSource的另一个实现类BeanPropertySqlParameterSource允许我们对bean对象进行封 装,并使用相应的bean对象的属性值作为命名参数符号的值。 如果images表对应的域对象(domain object)定义如下的话:
public class Image { private int id; private String filename; private byte[] entity; private String decription; public String getFileName() { return this.filename; } public String getDescription() { return this.description; } // other getters and setters ... }那么,我们可以使用BeanPropertySqlParameterSource对其进行封装然后作为参数传给NamedParameterJdbcTemplate进行数据访问:
DataSource dataSource = ...; NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); Image image = new Image(); image.setFilename("snow_image.jpg"); image.setDescription("nothing to say"); SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(image); int count = npJdbcTemplate.queryForInt(SQL, parameterSource);当然了,对于使用BeanPropertySqlParameterSource的情况,最好是方法参数传入bean对象的情况,在方法内部自己构造bean对象然后又通过BeanPropertySqlParameterSource封装,显然是有些繁琐了。
使用BeanPropertySqlParameterSource唯一需要注意的就是,SQL中的命名参数符号的名称应该与bean对象定义的属性名称一致。
其实,在有了JdbcTemplate的基础上,要实现NamedParameterJdbcTemplate就容易的多了,NamedParameterJdbcTemplate只需要提供JdbcTemplate之外的特性就可以。
NamedParameterJdbcTemplate内部持有一个 org.springframework.jdbc.core.JdbcOperations实例,JdbcTemplate是 JdbcOperations的唯一实现类,所以,说NamedParameterJdbcTemplate内部有一个JdbcTemplate更直接一 些。 顺理成章的,如果你已经有了一个JdbcTemplate的实例,而又想构造一个NamedParameterJdbcTemplate,你除了可以传入 相应的DataSource作为参数,当然也可以传入JdbcTemplate作为参数:
DataSource dataSource = ...; NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); // or you have got a JdbcTemplate JdbcTemplate jdbcTemplate = ...; NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);要通过NamedParameterJdbcTemplate获取内部持有的JdbcTemplate实例,可以通过其getJdbcOperations()方法:
NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); JdbcOperations jdbcOper = npJdbcTemplate.getJdbcOperations(); assertSame(jdbcTemplate,jdbcOper);
NamedParameterJdbcTemplate的getJdbcOperations()方法实际上是其父接口 org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations定义 的, 该接口定义的一组方法接受使用命名参数符号的SQL和SqlParameterSource或者Map作为方法参数,而具体的实现当然就由 NamedParameterJdbcTemplate来做啦。
NamedParameterJdbcTemplate的模板方法执行开始之后,首先借助于NamedParameterUtils工具类对传入的使用命名参数符号的SQL进行解析, 然后使用旧有的“?”占位符替换掉SQL中相应的命名参数符号,之后,根据替换后的SQL和从SqlParameterSource中解析出来的参数信息直接调用内部持有的JdbcTemplate实例来执行更新或者查询操作即可。
基本上,NamedParameterJdbcTemplate的实现就是在JdbcTemplate基础上多添加一层“解析”网。
SimpleJdbcTemplate是Spring为构建于Java5或者更高版本Java之上的应用程序提供的更加便利的“JdbcTemplate”实现, 所以,要使用SimpleJdbcTemplate,请先确认你的Java版本是否符合要求。
SimpleJdbcTemplate集JdbcTemplate和NamedParameterJdbcTemplate之功能于一身, 并且在这二者之上又添加了Java5之后引入的动态参数(varargs),自动拆箱解箱(autoboxing)和范型(generics)的支持。 现在,你可以通过动态参数(varargs)的形式取代Object[]的形式,可以利用自动拆箱解箱(autoboxing)避免原始类型到相应封装类 型的转换,可以声明强类型的返回值类型,而不是只有Object。
下面是使用SimpleJdbcTemplate后的部分代码演示,从中你能够看到Java5之后的一些特色功能:
DataSource dataSource = ...; SimpleJdbcTemplate sjt = new SimpleJdbcTemplate(dataSource); final LobHandler lobHandler = new DefaultLobHandler(); String SQL = "select * from images where filename=? and description=?"; ParameterizedRowMapper<Image> rowMapper = new ParameterizedRowMapper<Image>(){ public Image mapRow(ResultSet rs, int row) throws SQLException { Image image = new Image(); image.setId(rs.getInt(1)); image.setFilename(rs.getString(2)); image.setEntity(lobHandler.getBlobAsBytes(rs, 3)); image.setDescription(rs.getString(4)); return image; } }; Image image = sjt.queryForObject(SQL, rowMapper, "snow_image.jpg","nothing to say"); System.out.println(image.getDescription());另外,SimpleJdbcTemplate还声明一部分模板方法,可以接受使用命名参数符号作为占位符的SQL语句和以Map或者 SqlParameterSource的形式传入的参数,这部分模板方法会将执行逻辑委派给SimpleJdbcTemplate内部持有的一 NamedParameterJdbcTemplate实例, 这也就是为什么说,“SimpleJdbcTemplate集JdbcTemplate和NamedParameterJdbcTemplate之功能于一身” 的原因。 你可以通过SimpleJdbcTemplate.getNamedParameterJdbcOperations()方法获得 NamedParameterJdbcTemplate的实例引用,而通过 SimpleJdbcTemplate.getJdbcOperations()获得JdbcTemplate的实例引用:
SimpleJdbcTemplate sjdbcTemplate = ...; NamedParameterJdbcOperations npJdbcOper = sjdbcTemplate.getNamedParameterJdbcOperations(); JdbcOperations jdbcOper = sjdbcTemplate.getJdbcOperations();从这里可以看出来,SimpleJdbcTemplate的最终工作还是委派给了JdbcTemplate,他本身仅仅是在JdbcTemplate和NamedParameterJdbcTemplate基础之上又加了部分Java5的功能修饰。
如果说NamedParameterJdbcTemplate是在JdbcTemplate的基础上套了一层网,那么,SimpleJdbcTemplate则是又在NamedParameterJdbcTemplate的基础上套了另一层网而已。
Spring的数据访问框架在数据库资源的管理上全部采用Jdbc2.0标准之后引入的javax.sql.DataSource接口作为标准, 无论是从JdbcTemplate还是到各种ORM方案的集成,皆是如此。鉴于DataSource的重要地位,我们有必要对其做进一步的了解。
DataSource的基本角色是一个ConnectionFactory,所有的数据库连接将通过DataSource接口统一管理。 DataSource实现类根据功能强弱可以划分为以下三类:
-
简单的DataSource实现. 这种DataSource实现通常只提供作为ConnectionFactory角色的基本功能,更多时候,我们是使用这类的DataSource实现进 行开发或者测试, 而绝不会用于正式的生产环境。Spring在org.springframework.jdbc.datasource包下提供的了两个简单的 DataSource实现:
-
org.springframework.jdbc.datasource.DriverManagerDataSource. 从名字里你也看的出来,DriverManagerDataSource作为DataSource的提出主要为了替换最古老的基于 java.sql.DriverManager获取连接的方式。 如果你从最初的JDBC1.x走过来的话,你应该还记得最初进行数据库连接是如何做的(此处忽略了异常处理):
// 1. 初始化Driver类 Class.forName("driverClassName",true,getClass().getClassLoader()); // 2. 由DriverManager取得连接 String jdbcUrl = ...; Properties connectionInfo = new Properties(); connectionInfo.put("user",".."); connectionInfo.put("password",".."); ... Connetion con = DriverManager.getConnection(jdbcUrl, connectionInfo); ...
当你每次向DriverManager请求一个数据库连接的时候,DriverManager都会返回一个新的数据库连接给你。 实际上,DriverManagerDataSource就是对这种行为以DataSource标准接口进行封装后的产物,当你每次通过 DriverManagerDataSource的getConnection()方法请求连接的时候, DriverManagerDataSource也会每次返回一个新的数据库连接。也就是说,DriverManagerDataSource没有提供连 接缓冲池的功能,在某些情况下应该避免将其应用于生产环境。你可以通过编程的方式或者通过Spring的IoC容器来使用DriverManagerDataSource,唯一要做的就是提供必要的连接信息而已:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost/mysql?useUnicode=true&characterEncoding=utf8"/> <property name="username" value="..."/> <property name="password" value="..."/> </bean>
destroy-method="close"的作用我想就不用多说了吧?! -
org.springframework.jdbc.datasource.SingleConnectionDataSource. SingleConnectionDataSource是在DriverManagerDataSource的基础上构建的一个比较有意思的 DataSource实现, DriverManagerDataSource在每次请求的时候都返回新的数据库连接,而SingleConnectionDataSource则是每 次都返回同一个数据库连接, 也就是说,SingleConnectionDataSource只管理并返回一个数据库连接,singleton的 ConnectionFactory?
从SingleConnectionDataSource取得的Connection如果被调用方通过close()方法关闭之后,再次从SingleConnectionDataSource请求数据库连接的话,将抛出SQLException, 所以,如果我们想“即使从SingleConnectionDataSource返回的Connection对象的close()方法被调用,连接也不关闭” 的话,我们可以通过SingleConnectionDataSource的setSuppressClose(boolean)方法将 SingleConnectionDataSource的suppressClose设置为true, 此后,只要SingleConnectionDataSource不destroy,那么从它那里返回的Connection对象就将“万世长存”啦!
SingleConnectionDataSource提供了多个构造方法,你可以像DriverManagerDataSource那样构造SingleConnectionDataSource,也可以对现有的数据库连接进行封装:
Connection availableConnection = ...; SingleConnectionDataSource dataSource = new SingleConnectionDataSource(availableConnection,true);
对于遗留系统和现有系统的集成,这种方式或许会有意想不到的帮助。
-
-
拥有连接缓冲池的DataSource实现. 这一类的DataSource实现,除了提供作为ConnectionFactory角色基本功能之外,内部还会通过连接缓冲池对数据库连接进行管理, 生产环境下的DataSource全都属于这一类。 使用数据库连接缓冲池,可以在系统启动之初就初始化一定数量的数据库连接以备用,返回给客户端的Connection对象即使通过close()方法被关 闭, 实际上也只是被返回给缓冲池,而不是真正的被关闭,这极大的促进了数据库连接资源的复用,进而提高系统性能。
Jakarta Commons DBCP和C3P0可算是这一类DataSource实现中的代表(当然啦,像WebLogic,WebSphere,Jboss等应用服务器提供的 DataSource实现也属于这个范畴), 你可以在Standalone应用程序中使用它们,而不用非要绑定到应用服务器。 DBCP的使用我们之前已经多次领教了,编程方式还是IoC容器配置方式使用根据情况来定就行,这次,我们不妨看看C3P0的配置吧:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean>
ComboPooledDataSource属于C3P0提供的DataSource实现,除了可以指定基本的连接信息,你当然也可以指定像初始连接数,最小连接数,最大连接数等参数, 更多的信息参照C3P0的API文档即可。 -
支持分布式事务的DataSource实现类. 这一类的DataSource实现类确切一点儿说应该是javax.sql.XADataSource的实现类,从XADataSource返回的数据库 连接类型为javax.sql.XAConnection,而XAConnection扩展了javax.sql.PooledConnection, 所以你也看的出来,支持分布式事务的DataSource实现类同样支持数据库连接的缓冲。
除非我们的应用程序确实需要分布式事务,否则,我们没有必要使用这一类的DataSource实现,通常的“拥有连接缓冲池的DataSource实现类”就已经足够了。 而且,通常只有比较重量级的应用服务器才会提供“支持分布式事务的DataSource”,比如BEA的WebLogic或者IBM的Websphere等。
你可以在本地构造并持有相应的DataSource实现,你也可以将相应的DataSource绑定到命名服务,这完全取决于当前应用程序的使用场景, 不过,DataSource的位置将决定了你获取DataSource支持的不同方式。
如果你在当前应用程序的上下文中构造并持有了相应的DataSource实现,那么,你可以通过本地的DataSource引用对其进行访问。这将是我们最常用的方式,无论是在独立的应用程序中还是在应用服务器中。
只要将相应的DataSource实现类所在的jar包加入应用程序的Classpath,你就可以直接构造并访问它,而使用Spring的IoC 容器来管理本地的DataSource资源将是最为理想的方式,只要在容器的配置文件中进行简单的配置即可, 容器中任何需要该资源的对象都可以获得依赖注入。否则,你或许得自己构建一个专门用于该DataSource存取的Singleton对象,以供相应对象 调用。
我想,对于基于Spring的IoC容器配置和访问DataSource我们已经是轻车熟路了,所以,废话少说,接着看下一种DataSource的访问方式如何?
对于各种应用服务器提供的特有的DataSource实现,或者绑定到应用服务器命名服务的独立的DataSource实现,我们需要通过JNDI[8]对其进行访问。 对于运行于应用服务器的程序或者分布式应用来说,通过JNDI访问DataSource将是最为常见的方式。
在Spring的IoC容器中,我们可以通过org.springframework.jndi.JndiObjectFactoryBean对这些DataSource进行访问:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:env/myDataSource</value> </property> </bean>JndiObjectFactoryBean是一个FactoryBean实现,容器中对其引用获得的将是getObject()所返回的对象,而不是JndiObjectFactoryBean本身,在这里,当然就是我们要用的DataSource啦。
如果我们已经迁移到spring2.x,并且使用的是基于XSD的配置格式,那么我们可以直接使用<jndi:lookup>来查找应用服务器上的DataSource:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd"> <jndi:lookup id="dataSource" jndi-name="java:env/myDataSource"/> ... </beans>相对于通过直接指定JndiObjectFactoryBean来进行JNDI查找而言,基于XSD的方式显然简洁多了,不过所达到的效果是相同的,只不过,在使用之前,不要忘记将jee的命名空间加入配置文件就行了。
除了可以使用现有的各种DataSource实现,Spring在org.springframework.jdbc.datasource包下还提供了部分类帮助你自定义实现相应的DataSource, 前提当然是你的需求实在是比较特殊。
要实现一个新的DataSource,我们可以扩展 org.springframework.jdbc.datasource.AbstractDataSource,这是专门用于DataSource实 现的扩展基类, 它的一个直接实现类我们已经接触过了,就是DriverManagerDataSource,而SingleConnectionDataSource扩 展了DriverManagerDataSource,也算AbstractDataSource一家子了。
AbstractDataSource除了DriverManagerDataSource这一个直接子类外,在spring框架中还有一个 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource实现类, 这个类的作用比较特殊,但某些场景下应该很有用。AbstractRoutingDataSource会持有一组DataSource,当它的 getConnection()被调用的时候,会根据条件从这组DataSource中查找符合条件的DataSource, 然后调用查找取得的DataSource上的getConnection()。如果你的应用程序中有多个数据库,并且需要根据情况,让应用程序访问不同的 数据库的话,那么扩展并实现AbstractRoutingDataSource的一个子类将是你的最佳选择。
要扩展AbstractRoutingDataSource,实际上只需要实现一个查找方法的逻辑即可:
protected abstract Object determineCurrentLookupKey();每次getConnection()请求将被导向那个DataSource,将有这个方法的逻辑来决定。
AbstractRoutingDataSource的整个场景类似于下图所示:
当然,目前为止,都是spring提供的AbstractDataSource扩展类,当我们的应用需求比较特殊,找不到符合条件的 DataSource的时候,那么我们也可以像DriverManagerDataSource或者AbstractRoutingDataSource 那样, 直接扩展AbstractDataSource来实现我们自己的DataSource。不过,笔者目前为止还没有遇到类似需求场景,所以,如果你遇到的 话,那么,不要犹豫,just do it!
要自定义DataSource实现,除了直接扩展 org.springframework.jdbc.datasource.AbstractDataSource或者他的相关子类之外, 我们还有另一个选择,那就是org.springframework.jdbc.datasource.DelegatingDataSource。
DelegatingDataSource的作用在于,它自身持有一个其他的DataSource实例作为目标对象,当 getConnection()等方法调用发生的时候, DelegatingDataSource会将调用转发(或者说委派)给持有的这个DataSource实例。 DelegatingDataSource本身在转发这些调用之前没有做任何事情,这是没有任何意义的,要发挥DelegatingDataSource 的作用, 我们在实现DelegatingDataSource子类的时候,需要覆写(Override)相应的方法,在转发方法调用之前添加相应的自定义逻辑,这 听起来像是AOP的领域了,不过, 目标对象和则Joinpoint是限定死的。
DelegatingDataSource在spring框架内有几个现成的实现类:
-
org.springframework.jdbc.datasource.UserCredentialsDataSourceAdapter. UserCredentialsDataSourceAdapter可以为现有的DataSource加入验证信息, 通过UserCredentialsDataSourceAdapter的getConnection()方法获取Connection的时候, UserCredentialsDataSourceAdapter会根据自身的username和password信息调用自身持有的 DataSource目标对象的getConnection(username,password)方法以获取Connection, 如果没有username和passwor的信息,再调用没有参数的getConnection()。
-
org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy. TransactionAwareDataSourceProxy会对一个DataSource目标对象进行封装, 所有从TransactionAwareDataSourceProxy取得的Connection将自动加入Spring的事务管理(事务管理相关内容 将在下一章详述)。 要想加入spirng的事务管理,我们需要使用DataSourceUtils类进行Connection的管理(JdbcTemplate内部就是这么 做的)。 TransactionAwareDataSourceProxy内部在对DataSource目标对象的Connection管理上也是使用的 DataSourceUtils进行的,所以, 即使现在客户端使用Jdbc API直接从TransactionAwareDataSourceProxy取得Connection,该Connection也将纳入Spring的 事务管理:
DataSource dataSource = ...; Connection con = DataSourceUtils.getConnection(dataSource); // 两种方式取得的Connection都可以加入Spring管理的事务 TransactionAwareDataSourceProxy txDataSource = new TransactionAwareDataSourceProxy(dataSource); Connection txAwareConnection = txDataSource.getConnection();
更多TransactionAwareDataSourceProxy的信息可以参照javadoc。 -
org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy. 通过LazyConnectionDataSourceProxy取得的Connection对象是一个代理对象, 该代理对象可以保证当Connection被使用的时候才会从LazyConnectionDataSourceProxy持有的DataSource目 标对象上获取。
如果现在来实现一个DAO的话,我想,我们肯定不会像原来那样使用底层的JDBC API来实现它了,最起码,我们会使用相应的DataSource提供数据库连接,使用JdbcTemplate进行数据库操作:
public class GenericDao implements IDaoInterface { private DataSource dataSource; private JdbcTemplate jdbcTemplate; public GenericDao(DataSource ds,JdbcTemplate jt) { this.dataSource = ds; this.jdbcTemplate = jt; } public void update(DomainObject obj) { getJdbcTemplate().update(...); } ... // setters and getters }单个的DAO实现这么做是没有问题的,但当存在多个类似的DAO实现的时候,我们就给考虑重构这些DAO实现类,将DataSource和 JdbcTemplate全部提取到统一的超类中,不过,这部分工作spring已经为我们做了,它直接提供了 org.springframework.jdbc.core.support.JdbcDaoSupport作为所有基于Jdbc进行数据访问的DAO 实现类的超类。 所以,现在,我们的DAO直接继承JdbcDaoSupport就可以:
public class GenericDao extends JdbcDaoSupport implements IDaoInterface { public void update(DomainObject obj) { getJdbcTemplate().update(...); } ... // setters and getters }至此,基于JdbcTemplate方式进行数据访问的所有内容告一段落了,我们要进入下一单元,称作是“基于操作对象的Jdbc使用方式”!
Spring除了提供基于Template形式的JDBC使用方式,还对各种数据库操作以面向对象的形式进行建模,为我们使用JDBC进行数据访问提供了另一种视角。
在这种“基于操作对象的Jdbc使用方式”中,像查询,更新,调用存储过程的数据访问操作 被抽象为一个个的操作对象, 这些操作对象统一定义在org.springframework.jdbc.object包下,以 org.springframework.jdbc.object.RdbmsOperation作为整个操作对象体系的顶层抽象定义。
RdbmsOperation是一个抽象类,它提供了所有子类所需要的“公共设施”,包括当前数据库操作对应的SQL语句的声明,参数列表处理以及进行底层数据库操作所必需的JdbcTemplate实例等等。 所有的操作对象最终的数据访问都是通过JdbcTemplate进行的,这一点可以让你清楚,实际上,“基于操作对象的jdbc使用方式”与“基于JdbcTemplate的Jdbc使用方式”是统一的,只不过对待概念的视角上有所不同而已。
RdbmsOperation下根据数据访问操作分为三个主要分支,即查询操作对象分支,更新操作对象分支以及存储过程对象分支:
-
查询操作对象分支. org.springframework.jdbc.object.SqlQuery抽象类是查询操作对象分支的“首脑”, 它为子类提供了各种执行数据查询操作的方法实现,不过,查询结果的处理下放给子类来实现。
-
更新操作对象分支. org.springframework.jdbc.object.SqlUpdate是该分支的主要实现类,它是实体类,你可以直接使用它进行数据库更新操作, 也可以对其进行扩展,比如,在子类中提供基于强类型参数的更新方法定义。
-
存储过程对象分支. 在这个分支中,我们将更多的使用org.springframework.jdbc.object.StoredProcedure,而不是他的父类 org.springframework.jdbc.object.SqlCall, 因为SqlCall的主要作用是根据调用信息构建相应的CallableStatementCreator, StoredProcedure在SqlCall的基础上提供了执行调用存储过程的方法定义。
现在,让我们详细看一下每一个分支下,spring具体都为我们提供了哪些可用的操作对象...
下图是整个查询操作对象体系:
相对于其他操作对象定义分支来说,查询操作对象的定义体系看起来要“繁盛”的多。
SqlQuery作为MappingSqlQueryWithParameters的父类,只定义了执行数据查询操作的各种方法,而处理查询结果的 工作则下放给了具体子类,相应子类需要实现SqlQuery定义的如下抽象方法以返回处理查询结果的RowMapper对象:
protected abstract RowMapper newRowMapper(Object[] parameters, Map context);MappingSqlQueryWithParameters对象提供了该方法的实现,返回了MappingSqlQueryWithParameters的内部类RowMapperImpl的实例,作为处理查询结果所需要的RowMapper。
protected class RowMapperImpl implements RowMapper { private final Object[] params; private final Map context; /** * Use an array results. More efficient if we know how many results to expect. */ public RowMapperImpl(Object[] parameters, Map context) { this.params = parameters; this.context = context; } public Object mapRow(ResultSet rs, int rowNum) throws SQLException { return MappingSqlQueryWithParameters.this.mapRow(rs, rowNum, this.params, this.context); } }不过,RowMapperImpl在实现RowMapper的mapRow方法的时候,将具体的处理逻辑又给转发了,而且,转发给了MappingSqlQueryWithParameters的mapRow方法:
protected abstract Object mapRow(ResultSet rs, int rowNum, Object[] parameters, Map context) throws SQLException;所以,要使用MappingSqlQueryWithParameters进行数据查询,我们得提供它的子类,然后实现这个mapRow方法以处理查询结果。
还记得我们前面的fx_news表吗?现在我们要使用MappingSqlQueryWithParameters对其进行查询。 首先,我们得定义MappingSqlQueryWithParameters的一个子类,为mapRow方法提供合适的逻辑以处理查询结果:
public class FXNewsQueryWithParameters extends MappingSqlQueryWithParameters { private static final String QUERY_SQL = "select * from fx_news where news_title = ?"; public FXNewsQueryWithParameters(DataSource dataSource) { super(dataSource, QUERY_SQL); declareParameter(new SqlParameter(java.sql.Types.VARCHAR)); compile(); } @Override protected Object mapRow(ResultSet rs, int row, Object[] parameters, Map context) throws SQLException { FXNewsBean newsBean = new FXNewsBean(); newsBean.setNewsId(rs.getString(1)); newsBean.setNewsTitle(rs.getString(2)); newsBean.setNewsBody(rs.getString(3)); return newsBean; } }关于FXNewsQueryWithParameters的定义,我们有几点需要说明,这些点通常将同样应用于其他操作对象:
-
所有的操作对象都需要一个DataSource用于访问数据库,底层使用JdbcTemplate进行最终的数据访问操作的事实,让操作对象需要一个DataSource变得很自然,不是吗?
-
操作对象(RdbmsOperation)在使用之前,需要调用compile()方法对操作对象所必需的各种参数进行检查和设置,只有 compile()通过之后,当前操作对象才可以使用,所以,我们在FXNewsQueryWithParameters的构造方法最后调用了 compile()方法, 这样FXNewsQueryWithParameters一旦构造完成即可付诸使用。
-
declareParameter()方法用于为操作对象指明所对应的SQL语句中各种参数类型,该方法接受org.springframework.jdbc.core.SqlParameter及其子类作为参数。 因为我们的SQL中有“news_title”这一参数,所以,我们使用declareParameter()告知操作对象这一参数相关信息。
注意,declareParameter()必须在compile()之前进行!
-
mapRow方法与使用JdbcTemplate结合RowMapper处理查询结果没有太大区别,唯一需要关注的是最后两个方法参数,不过,在此之前,我们先来看一下如何使用已经定义完的FXNewsQueryWithParameters!
DataSource dataSource = ...; // DBCP or C3P0 or whatever FXNewsQueryWithParameters query = new FXNewsQueryWithParameters(dataSource); List news = query.execute("FX Market Rocks"); for(int i=0,size=news.size();i<size;i++) { FXNewsBean bean = (FXNewsBean)news.get(i); System.out.println(bean); }如果多个DAO都需要用到FXNewsQueryWithParameters所提供的查询逻辑的话,你可以将同一个 FXNewsQueryWithParameters实例分别注入这些需要的对象当中, compile()后的FXNewsQueryWithParameters是线程安全的。
有关查询操作对象所定义更多的查询方法定义,可以参照spring的Javadoc文档,现在,我们要回头看一下MappingSqlQueryWithParameters的mapRow方法最后两个参数:
-
Object[] parameters. 调用execute方法或者其他查询方法所传入的参数值列表,比如,我们在上面调用execute方法的时候传入了一个参数值,“FX Market Rocks”, 那么,我们就可以通过parameters获取这个值,如果我们在mapRow中查看parameters[0]的话,对应的应该就是“FX Market Rocks”。
不过,处理查询结果的时候,好像很少要用到SQL对应的参数值吧?!
-
Map context. 对于查询操作对象所定义的查询方法来说,每一组查询方法通常对应一个可以传入Map作为参数的重载方法:
List execute(Object[] params) List execute(Object[] params, Map context) ... Object findObject(String p1) Object findObject(String p1, Map context) ...
该参数的作用在于,可以为查询结果的处理传入更多信息,你可以在mapRow方法中使用这些传入的信息对查询结果进行某些处理。比如,我们在查询获得新闻信息的基础上,要在新闻标题上追加某个字符串前缀,然后再返回,我们就可以通过这个Map参数传入所需要的前缀信息:
FXNewsQueryWithParameters query = new FXNewsQueryWithParameters(dataSource); Map context = new HashMap(); context.put("PREFIX", "FX"); List news = query.execute("title",context); for(int i=0,size=news.size();i<size;i++) { FXNewsBean bean = (FXNewsBean)news.get(i); System.out.println(bean); }
我们现在使用的execute接收参数值和传入的context信息,关于context传入的信息,你就可以通过FXNewsQueryWithParameters的mapRow方法最后一个参数取得:@Override protected Object mapRow(ResultSet rs, int row, Object[] parameters, Map context) throws SQLException { String prefix = (String)context.get("PREFIX"); FXNewsBean newsBean = new FXNewsBean(); newsBean.setNewsId(rs.getString(1)); newsBean.setNewsTitle(prefix+rs.getString(2)); newsBean.setNewsBody(rs.getString(3)); return newsBean; }
当然啦,context传入的信息并不限于简单的字符串前缀啦,这要根据你的应用场景来决定。
MappingSqlQuery继承了MappingSqlQueryWithParameters,然后实现了MappingSqlQueryWithParameters拥有四个参数的mapRow方法:
protected final Object mapRow(ResultSet rs, int rowNum, Object[] parameters, Map context) throws SQLException { return mapRow(rs, rowNum); }不过,MappingSqlQuery对该方法的实现实际上仅仅限于去掉了最后的两个方法参数,然后又重新为子类暴露了只有前两个方法参数的mapRow方法:
protected abstract Object mapRow(ResultSet rs, int rowNum) throws SQLException;要通过扩展MappingSqlQuery进行数据查询的话,只需要实现拥有两个参数的mapRow方法就行啦。
定义同样的查询,使用MappingSqlQuery进行查询对象定义的话,看起来如下:
public class FXNewsQuery extends MappingSqlQuery { private static final String QUERY_SQL = "select * from fx_news where news_title = ?"; public FXNewsQuery(DataSource dataSource) { super(dataSource, QUERY_SQL); declareParameter(new SqlParameter(Types.VARCHAR)); compile(); } @Override protected Object mapRow(ResultSet rs, int row) throws SQLException { FXNewsBean newsBean = new FXNewsBean(); newsBean.setNewsId(rs.getString(1)); newsBean.setNewsTitle(rs.getString(2)); newsBean.setNewsBody(rs.getString(3)); return newsBean; } }除了要实现的mapRow方法不同,与直接继承MappingSqlQueryWithParameters并无二致。 至于怎么使用这个FXNewsQuery,我想,跟FXNewsQueryWithParameters的使用那简直是一模一样的。
通过继承MappingSqlQuery实现查询操作对象,与结合RowMapper使用JdbcTemplate实际上应该是对等的, 所以,如果在结果处理上没有其他附加条件的话,MappingSqlQuery将是我们最经常使用的查询操作对象实现了。
SqlFunction是职责更加专一的查询操作对象,对应的是只返回一行并且一列的查询结果的查询。 SqlFunction本身的目的就是为了调用只返回单一查询结果的简单查询,比如“select count(*) from fx_news”或者“select sysdate from dual”之类。
不像其他的SqlQuery子类那样都是抽象类,SqlFunction本身为实体类,你可以直接实例化SqlFunction并调用相应的方法查询方法:
DataSource dataSource = ...; // DBCP or C3P0 or whatever SqlFunction function = new SqlFunction(dataSource,"select count(*) from fx_news"); function.compile(); int count = function.run(); // or SqlFunction f = new SqlFunction(dataSource,"select current_timestamp() from fx_news limit 1"); f.compile(); Object currentTime = f.runGeneric(); assertTrue(currentTime instanceof java.sql.Timestamp);SqlFunction的run()方法返回int型结果,runGeneric()方法返回Object型结果,二者都可以接收参数。
因为SqlFunction本身同样属于RdbmsOperation,所以使用之前,不要忘记调用它的compile()方法!
UpdatableSqlQuery主要对应可更新结果集的查询,通过它,我们可以对查询后的结果进行更新操作。通常情况下,如果数据库中的某些数 据需要统一的更新, 我们可以将这些数据查询出来,然后再分批更新回数据库。不过,既然我们的主要目的是更新,而查询的结果是次要的,那么,我们可以在可更新结果集上进行类似 操作, 而UpdatableSqlQuery就是为此而生。
要使用UpdatableSqlQuery,只需要继承它,然后实现updateRow方法即可:
protected abstract Object updateRow(ResultSet rs, int rowNum, Map context) throws SQLException;假设我们要将fx_news表中所有新闻标题的首字母全部变为大写,那么,我们可以有类似如下的UpdatableSqlQuery实现类:
public class CapitalTitleUpdateableSqlQuery extends UpdatableSqlQuery { public CapitalTitleUpdateableSqlQuery(DataSource dataSource,String sql) { super(dataSource,sql); compile(); } @Override protected Object updateRow(ResultSet rs, int row, Map context) throws SQLException { String title = rs.getString("news_title"); rs.updateString("news_title", StringUtils.capitalize(title)); return null; } }要使用该CapitalTitleUpdateableSqlQuery实现类,跟其他的SqlQuery类似,只不过,现在的查询结果对于我们来说已经没有太大用处了:
DataSource dataSource = ...; // DBCP or C3P0 or whatever String sql = "select * from fx_news"; CapitalTitleUpdateableSqlQuery updatableQuery = new CapitalTitleUpdateableSqlQuery(dataSource,sql); updatableQuery.execute();可能你也注意到了,updateRow方法最后一个参数为“Map context”,它的作用 跟我们在讲解MappingSqlQueryWithParameters的mapRow方法所提到的最后一个参数是一样的。 在我们使用可更新结果集对查询结果进行更新的过程中,如果需要某些外部信息的话,我们可以在调用UpdatableSqlQuery数据访问操作方法的时 候通过Map传入,而在updateRow方法中通过“Map context”参数取得, 使用方式可以参照MappingSqlQueryWithParameters部分针对该参数的使用实例。
在JdbcTemplate部分,我们讲解了如何使用JdbcTemplate对LOB型数据进行查询,现在,我们要说的是使用查询操作对象进行LOB型数据的查询。
实际上,不管是使用JdbcTemplate还是使用查询操作对象,对Lob数据的处理都是由具体的LobHandler来处理的, 要在查询操作对象中对LOB型的查询结果进行处理,只要为具体的查询操作对象提供相应的LobHandler就行了。
同样是对images表进行查询,使用LobHandler处理LOB数据的查询操作对象如下:
public class LobHandlingSqlQuery extends MappingSqlQuery { private static final String SQL = "select * from images where id=?"; private LobHandler lobHandler = new DefaultLobHandler(); public LobHandlingSqlQuery(DataSource dataSource) { setDataSource(dataSource); setSql(SQL); declareParameter(new SqlParameter(Types.INTEGER)); compile(); } @Override protected Object mapRow(ResultSet rs, int row) throws SQLException { Image image = new Image(); image.setId(rs.getInt(1)); image.setFilename(rs.getString(2)); image.setEntity(lobHandler.getBlobAsBytes(rs, 3)); image.setDescription(rs.getString(4)); return image; } public LobHandler getLobHandler() { return lobHandler; } public void setLobHandler(LobHandler lobHandler) { this.lobHandler = lobHandler; } }当前的查询操作对象LobHandlingSqlQuery扩展了MappingSqlQuery,根据具体应用情况,你同样可以扩展其他 SqlQuery类。 在LobHandlingSqlQuery中,我们使用LobHandler将BLOB型的图像文件数据以byte[]的形式取得,供查询调用方处理:
LobHandlingSqlQuery lobQuery = new LobHandlingSqlQuery(dataSource); List images = lobQuery.execute(1); Image image = (Image)images.get(0); FileUtils.writeByteArrayToFile(new File(image.getFilename()), image.getEntity());在这里,我们是将从数据库中取得的转换为byte[]的BLOB数据写入了文件系统。
spring提供的用于更新的操作对象主要有两个,即SqlUpdate和BatchSqlUpdate,前者主要用于基本的更新操作,后者则主要用于进行批量更新操作。
下面让我们详细看一下这两种更新操作对象...
SqlUpdate与SqlQuery同样继承自SqlOperation,所以,它的所有的更新操作全部基于SqlOperation父类所返回的PreparedStatementCreator进行。
SqlUpdate是实体类,我们可以直接使用它进行数据更新,而不用必须继承它然后通过子类来使用:
String updateSql = "update fx_news set news_title=? where news_id=?"; SqlUpdate sqlUpdate = new SqlUpdate(dataSource,updateSql); sqlUpdate.declareParameter(new SqlParameter(Types.VARCHAR)); sqlUpdate.declareParameter(new SqlParameter(Types.INTEGER)); sqlUpdate.compile(); int affectedRows = sqlUpdate.update(new Object[]{"New Title",new Integer(1)});直接使用SqlUpdate需要注意的就是,你需要每次根据情况通过declareParameter声明相应的SQL占位符参数,并且最后,不要忘了最主要的,一定要调用compile方法之后才可以调用相应的update方法进行数据更新操作。
除了直接使用SqlUpdate进行数据更新,我们同样可以通过继承的方式来使用它,这样我们可以得到的好处有:
-
封装具体的更新操作,将初始化SqlUpdate的各个步骤统一到一处管理,避免遗漏(通常,declareParameter和compile方法在直接使用的时候难免出现遗漏的状况)。
-
提供强类型参数的更新操作方法以替代弱类型参数的更新方法,进一步强化调用方与被调用方的契约关系。
public class NewsTitleSqlUpdate extends SqlUpdate { private static final String TITLE_UPDATE_SQL = "update fx_news set news_title=? where news_id=?"; public NewsTitleSqlUpdate(DataSource dataSource) { super(dataSource,TITLE_UPDATE_SQL); declareParameter(new SqlParameter(Types.VARCHAR)); declareParameter(new SqlParameter(Types.INTEGER)); compile(); } public int updateTitleById(String newTitle,int id) { return update(new Object[]{newTitle,new Integer(id)}); } }而现在对于调用方来说,使用NewsTitleSqlUpdate进行数据更新要比直接使用SqlUpdate要简洁的多:
NewsTitleSqlUpdate update = new NewsTitleSqlUpdate(dataSource); int affectedRows = update.updateTitleById("new title", 1);对于是直接使用SqlUpdate还是子类化后再使用,这完全可以根据应用的具体情况来决定,如果某个更新操作的可重用性很强, 那么针对该更新操作,子类化SqlUpdate后再使用比较合适;而如果某个更新操作在很少的场景用到,那么直接使用SqlUpdate则是比较合适的。
在Spring中,除了使用JdbcTemplate进行数据的批量更新之外,BatchSqlUpdate则为我们提供了另一种选择。 BatchSqlUpdate底层也是JdbcTemplate结合BatchPreparedStatementSetter进行批量更新,没有更多的 奥秘,所以,我们还是直接来看BatchSqlUpdate的使用吧!
BatchSqlUpdate同SqlUpdate一样也是实体类,所以,我们可以直接使用它。如果我们要将获得的新闻条目批量更新到数据库,我们可以使用如下的代码进行:
public void batchInsertFXNews(List<FXNewsBean> newsList) { BatchSqlUpdate batchUpdate = new BatchSqlUpdate(dataSource); batchUpdate.setSql("INSERT INTO fx_news(news_id, news_title, news_body) VALUES(?, ?, ?)"); batchUpdate.declareParameter(new SqlParameter(Types.BIGINT)); batchUpdate.declareParameter(new SqlParameter(Types.VARCHAR)); batchUpdate.declareParameter(new SqlParameter(Types.LONGVARCHAR)); batchUpdate.compile(); for(FXNewsBean bean : newsList) { batchUpdate.update(new Object[]{bean.getNewsId(),bean.getNewsTitle(),bean.getNewsBody()}); } batchUpdate.flush(); }我们使用BatchSqlUpdate的update方法将数据添加到批量更新的队列中,当队列数量等于batchSize的时候,将会自动触发批量更新 操作,否则,数据仅仅就是被添加到更新队列而已, 所以,最后需要调用flush()方法以确保所有的记录都更新到数据库。从这个角度来看,BatchSqlUpdate就像货车一样,货物装满即发车(数 据量达到batchsize,就进行批量更新), 当最后就剩下一点儿货物的时候,因为没有再多的货物可装载了,同样也得发这最后一趟,flush()方法就是这最后一趟车,确保即使数据量没有达到规定的 batch size,剩下的数据也得更新到数据库中。
你可以通过setBatchSize(int)方法对batchSize进行调整,默认为5000,只有当默认值不足以满足应用的批量更新性能要求 的时候,你才有必要对该数值进行调优。 另外,与SqlUpdate的使用一样,如果感觉直接使用BatchSqlUpdate过于繁琐的话,你也可以通过扩展它对其进行封装。
借助于LobHandler,我们同样可以使用更新操作对象对LOB型数据进行更新操作。
假设数据库中存放在images表的数据,对应某个文件名的图片文件需要替换,我们就可以提供一个支持LOB数据更新的SqlUpdate,对旧有的图片数据进行更新替换:
public class ImageLobDataUpdate extends SqlUpdate { private static final String SQL = "update images set entity=? where filename=?"; private LobHandler lobHandler = new DefaultLobHandler(); public ImageLobDataUpdate(DataSource dataSource) { super(dataSource, SQL); declareParameter(new SqlParameter(Types.BLOB)); declareParameter(new SqlParameter(Types.VARCHAR)); compile(); } public int replaceImageData(String filename,File imageFile) throws IOException { InputStream ins = null; try { ins = new FileInputStream(imageFile); SqlLobValue lobValue = new SqlLobValue(ins,(int)imageFile.length(),getLobHandler()); return update(new Object[]{lobValue,filename}); } finally { IOUtils.closeQuietly(ins); } } public LobHandler getLobHandler() { return lobHandler; } public void setLobHandler(LobHandler lobHandler) { this.lobHandler = lobHandler; } }关于ImageLobDataUpdate有几点需要说明:
-
要对LOB数据进行处理,我们需要一个LobHandler,所以,我们开始声明了一个LobHandler,默认情况下采用DefaultLobHandler,如果数据库是Oracle,那么可以通过setLobHandler替换掉默认的LobHandler;
-
因为我们要更新的“entity”字段是BLOB型,所以,我们在使用declareParameter声明SQL中相应占位符参数的类型的时候,采用的是java.sql.Types.BLOB:
declareParameter(new SqlParameter(Types.BLOB));
其他的LOB类型处理需要根据具体应用场景进行调整。 -
在更新操作对象中,要为LOB型占位符参数提供参数值,需要使用 org.springframework.jdbc.core.support.SqlLobValue, SqlLobValue将使用给定的LobHandler对具体的Lob数据进行处理。SqlLobValue的javadoc中同样有结合 SqlLobValue和JdbcTemplae进行Lob数据更新的实例。
ImageLobDataUpdate update = new ImageLobDataUpdate(dataSource); update.replaceImageData("snow_image.jpg", new File("43.jpg"));就是这么简单!(当然,不要忘记异常的处理哦!)
在JdbcTemplate部分我们介绍过如何使用JdbcTemplate和相应的Callback接口来调用存储过程, 现在,我们可以将JdbcTemplate和那些Callback接口放在脑后,因为专门针对存储过程调用的操作对象为我们屏蔽了这些烦恼。
org.springframework.jdbc.object.StoredProcedure是对应存储过程调用的操作对象,它通过其父类 org.springframework.jdbc.object.SqlCall获得相应的底层API支持 (CallableStatementCreator), 然后在此基础之上构建了调用存储过程的执行方法。
StoredProcedure是抽象类,所以需要实现相应子类以封装对特定存储过程的调用,还记得我们在讲解JdbcTemplate调用存储过程时候定义的存储过程吗?
CREATE PROCEDURE CountTable(IN tableName varchar(1000),OUT sqlStr varchar(1000) , INOUT v INT) BEGIN set @flag = v; set @sql = CONCAT('select count(*) into @res from ' , tableName , ' where ACTIVE_FLAG=?'); PREPARE stmt FROM @sql; EXECUTE stmt using @flag; DEALLOCATE PREPARE stmt; set v = @res; set sqlStr = @sql; END通过继承StoredProcedure,我们可以为该存储过程的调用提供一个对应的操作对象:
public class CountTableStoredProcedure extends StoredProcedure { private static final String PROCEDURE_NAME = "CountTable"; public static final String IN_PARAMETER_NAME = "tableName"; public static final String OUT_PARAMETER_NAME = "sqlStr"; public static final String INOUT_PARAMETER_NAME = "v"; public CountTableStoredProcedure(DataSource dataSource) { super(dataSource,PROCEDURE_NAME); // setFunction(true); declareParameter(new SqlParameter(IN_PARAMETER_NAME,Types.VARCHAR)); declareParameter(new SqlOutParameter(OUT_PARAMETER_NAME,Types.VARCHAR)); declareParameter(new SqlInOutParameter(INOUT_PARAMETER_NAME,Types.INTEGER)); compile(); } public CountTableResult doCountTable(String tableName,Integer v) { Map paraMap = new HashMap(); paraMap.put(IN_PARAMETER_NAME, tableName); paraMap.put(INOUT_PARAMETER_NAME, v); Map resultMap = execute(paraMap); CountTableResult result = new CountTableResult(); result.setSql((String)resultMap.get(OUT_PARAMETER_NAME)); result.setCount((Integer)resultMap.get(INOUT_PARAMETER_NAME)); return result; } }关于该存储过程操作对象,部分细节我们有必要关注一下:
-
存储过程操作对象对应的SQL是存储过程的名称,而不是真正意义上的SQL语句,当我们调用compile方法的时候, StoredProcedure的父类SqlCall会根据你提供的存储过程名称拼装真正意义上的符合SQL92标准的存储过程调用语句, 类似于“{ call CountTable(?,?,?) }”的形式。
因为我们的CountTableStoredProcedure只针对CountTable存储过程调用,所以,该存储过程的名称我们在类一开始就声明为常量:
private static final String PROCEDURE_NAME = "CountTable";
如果有多个存储过程的参数顺序相同,结果处理也一样的话,你也可以将存储过程的名称声明为变量,这完全要取决于具体的应用场景。 -
在构造方法中,我们将“setFunction(true);”注释掉了,因为我们调用的CountTable不是一个Function, 如果你要调用的存储过程类型为Function的话,你需要通过该方法将“function”的值设置为true,以告知StoredProcedure在处理调用的时候要区别对待。
-
在complie之前通过declareParameter声明参数,这几乎是雷打不动的惯例,不过,在StoredProcedure中使用declareParameter的时候却要有所注意了:
-
针对存储过程参数类型为IN,OUT和INOUT不同,declareParameter接受的参数类型也应该是SqlParameter,SqlOutParameter和SqlInOutParameter;
-
SqlParameter,SqlOutParameter和SqlInOutParameter的相应实例在构造的时候,必须指定对应的参数名称,因为在调用存储过程的时候, 需要根据名称传入参数,更需要根据名称取得调用结果;
-
-
StoredProcedure提供了execute方法执行存储过程调用,通过Map的形式传入调用所需要的IN或者INOUT类型的参 数值, 所以,在构建参数Map的时候,该Map中的Key应该与declareParameter时候声明的参数名称相同; 另外,execute执行后返回的结果也是Map形式,从该结果Map中取得具体的结果值的时候,也是通过declareParameter中声明的 OUT/INOUT参数名作为key来获取的, 所以,这就是我们将各个参数的名称在类定义的开始声明为常量的原因:
public static final String IN_PARAMETER_NAME = "tableName"; public static final String OUT_PARAMETER_NAME = "sqlStr"; public static final String INOUT_PARAMETER_NAME = "v";
无论是输入参数Map还是输出参数结果对应的Map,他们中的Key应该与通过declareParameter方法声明的参数名称一一对应。
对于存储过程的调用者来说,它的代码现在可以简洁到两行代码:
// DataSource dataSource = ...; CountTableStoredProcedure storedProcedure = new CountTableStoredProcedure(dataSource); CountTableResult result = storedProcedure.doCountTable("tableName",1); ...漂亮多了,不是吗?
StoredProcedure提供了两个execute方法执行存储过程的调用,一个就是我们刚才使用的通过Map提供输入参数的execute 方法,另一个则是使用ParameterMapper类型提供输入参数的execute方法。 那么,为什么要提供这个使用ParameterMapper类型提供输入参数的execute方法那?
ParameterMapper定义的callback方法暴露了相应的Connection,如果说在构造输入参数列表的时候,必须用到 Connection的话, ParameterMapper恰好可以提供支持。比如,Oracle中定义的一存储过程,接收数组类型作为参数,而在oracle中,你只能通过 Oracle.sql.ARRAY和相应的Oracle.sql.ArrayDescriptor来定义数组类型的参数, ARRAY和ArrayDescriptor都需要用到相应的Connection进行构造。所以,对于Oracle中需要使用数组传入参数的存储过程来 说,我们可以通过如下类似代码进行调用:
public class OracleStoredProcedure { ... public Map call(...) { ParameterMapper paramMapper = new ParameterMapper(){ public Map createMap(Connection connection) throws SQLException { Map inMap = new HashMap(); ... Integer[] params = new Integer[]{new Integer(1),new Integer(2)}; ArrayDescriptor desc = new ArrayDescriptor("numbers", connection); ARRAY nums = new ARRAY(desc, connection, params); inMap.put("ArrayParameterName", nums); ... return inMap; }}; return execute(paramMapper); } }当然啦,我们的CountTableStoredProcedure在调用存储过程的时候也可以使用ParameterMapper传入相应的调用参数, 只不过,ParameterMapper的createMap方法暴露的Connection对于我们来说没有太大用处罢了。
spring对当前各种流行的ORM解决方案的集成主要体现在以下几个方面:
-
统一的资源管理方式. 因为各种ORM在使用过程中都会遇到跟JDBC相似的资源管理问题,所以,Spring框架以一致的方式,对各种ORM的使用进行封装, 从而使得在整个spring框架中,不管你是使用Jdbc还是使用orm,他们的使用方式以及资源管理方式是以统一的方式进行的。
-
特定于ORM的数据访问异常到Spring统一异常体系的转译. 我们之前已经说过spring为什么要提供一个统一的异常体系了,为了能够让客户端只关注这个异常体系而不用过多的关注当前应用的是何种特定的数据访问技术, spring在集成各种ORM解决方案的时候自然要将他们特定的数据访问异常进行转译,以将他们统一纳入“spring的统一异常层次体系”之麾下。
-
统一的数据访问事务管理及控制方式. spring对最各种数据访问方式(不光是ORM)所提供的另一种统一的“公共设施”就是它的事务管理抽象层, 通过这个事务管理抽象层,可以对各种数据访问方式的特定事务管理进行接管,然后以一种统一的方式进行管理和控制。
这部分内容我们将纳入下一章进行详细讲解,本章只关注数据访问相关内容,所以,有关统一的事务管理相关内容在本章不做牵涉。
Spring框架提供集成支持的ORM解决方案包括Hibernate,iBatis,JDO,Oracle Toplink,Apache OJB等,不过,随着spring版本的升级,在spring核心包里的支持范围在逐渐缩小, 对旧有版本ORM解决方案的支持转给了Spring Modules子项目,到spring2.5之后,spring核心包提供的ORM集成包括Hibernate3,iBatis2,JDO,JPA以及 Oracle的Toplink。 我们不打算对所有这些ORM的集成情况都进行详细介绍,因为那样显得很没有必要,在集成的方式上,几乎可以用“千篇一律” 来形容。但是,spring的集成方式比较统一并不意味着所有的ORM解决方案是一样的, 你应该根据具体的场景来决定使用何种ORM解决方案,政治上的,经济上的,技术上的,而这些好像与Spring对ORM解决方案的集成没有太多关 系,Spring对ORM的集成只是为了能够让你在使用ORM的过程中活得更加舒服一些而已。
以下内容将主要围绕Spring对Hibernate3和iBatis2这两种ORM方案的集成进行,最后会对其他ORM方案的集成进行一定的概 述,以便当你要使用这些ORM方案的时候,可以了解如何从Spring这里获得支持。 现在,就让我们开始这趟Spring中对各种ORM解决方案的集成之旅吧!
在当今Java业界各种ORM解决方案之中,Hibernate凭借其先期的优势以及后期继续跟进,无疑已经算是基于ORM进行数据访问的事实标准 了。 笔者2004年中开始接触Hibernate,并应用于后继项目的开发,虽然当时仅仅2.1版本,但以深刻感受到Hibernate之强大,随着 Hibernate3的发布,更是加入了更多特性。
但是强大归强大,在具体项目实践当中如何有效的使用hibernate却是一个值得探讨的问题。
Note
本节内容侧重于Spring框架如何对Hibernate的集成,所以,不会对Hibernate各种特性进行事无巨细的介绍, 有关hibernate的更多内容请参考官方文档或者相关书籍,Manning出版社的《Hibernate In Action》或者国内出版的《深入浅出Hibernate》都是介绍Hibernate的不错的书籍。
1.3.1.1. 旧日“冬眠”[9]时光
现在说起3,4年前的Hibernate项目实践,可能有人会对此嗤之以鼻,不过,子曾经曰过,“温故而知新”嘛,从过去吸取养分继续前进才是我们的最终目的。 不过,即使在去年的项目中,我依然可以看到有的项目因为类似的Hibernate使用方式导致数据库泄漏事件,甚至在很长一段时间内才清理干净,所以,这就更加坚定了我要旧事重提的信心。
整个故事的开端,我们需要先从Hibernate参考文档所提供的Session管理工具类说起(摘自Hibernate2.1.8参考文档):
public class HibernateUtil { private static Log log = LogFactory.getLog(HibernateUtil.class); private static final SessionFactory sessionFactory; static { try { // Create the SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { log.error("Initial SessionFactory creation failed.", ex); throw new ExceptionInInitializerError(ex); } } public static final ThreadLocal session = new ThreadLocal(); public static Session currentSession() throws HibernateException { Session s = (Session) session.get(); // Open a new Session, if this Thread has none yet if (s == null) { s = sessionFactory.openSession(); session.set(s); } return s; } public static void closeSession() throws HibernateException { Session s = (Session) session.get(); session.set(null); if (s != null) s.close(); } }HibernateUtil的作用在于对Session的初始化,获取以及释放的进行统一管理, 坦诚的说,单单从HibernateUtil本身来说,我们没有太多可以指摘的,但是,当大部分人都按照以下的实例代码的样子来使用 HibernateUtil的时候,问题就比较容易找你麻烦了:
Session session = HibernateUtil.currentSession(); Transaction tx= session.beginTransaction(); Cat princess = new Cat(); princess.setName("Princess"); princess.setSex('F'); princess.setWeight(7.4f); session.save(princess); tx.commit(); HibernateUtil.closeSession();仅仅关注这段使用代码好像没有什么不妥之处,可是,以同样的方式铺开到整个团队的开发中的话,那就真的不妥了!
对于一个团队来说,工作的分配通常是按照功能模块划分的,而不是按照应用程序的分层划分的,这就意味着, 每一个开发人员通常要实现所负责模块每一层的对象实现,具体点儿说, 每一个开发人员都需要负责自己功能模块的那一部分的Dao以及Service层对象开发。 为了能够给每一个开发人员开发DAO以及Service对象提供适度的“公共设施”, 我们通常会将Hibernate的使用进行适当的封装,将公用的一些行为抽象到父类中定义,这样,我们就有了类似如下的DAO父类实现(忽略了异常处理和其它细节):
public abstract class AbstractHibernateDao { private Session session = null; private Transaction transaction = null; public AbstractHibernateDao() throws HibernateException { session = HibernateUtil.currentSession(); } public Session getCurrentSession() { return this.session; } public Transaction beginTransaction() { return getCurrentSession().beginTransaction(); } public void commit() { if(this.transaction != null) this.transaction.commit(); } public void rollbck() { if(this.transaction != null) this.transaction.rollback(); } public void closeSession() { HibernateUtil.closeSession(); } }开发人员要实现DAO,需要继承该AbstractHibernateDao,并在构造方法中调用父类的构造方法,这样一旦子类实例化完毕,即可获得相应的Session进行数据访问:
public class FooHibernateDao extends AbstractHibernateDao implements IBarDao { public FooHibernateDao() { super(); } public void saveOperation(Object po) { getCurrentSession().save(po); } .... }因为事务控制通常在Service层做,所以,FooHibernateDao中没有过多牵扯事务特定的事务API以及session管理的代码,不过, 当我们现在来看相应的Service中加入这些代码之后,就会发现,以这种方式来使用Hibernate会是怎么一种糟糕的体验:
public class FooService { private FooHibernateDao dao; public void someBusinessLogic() throws Exception { try { getDao().beginTransaction(); Object mockPO = new Object(); getDao().saveOperation(mockPO); getDao().commit(); } catch(HibernateException e) { getDao().rollbck(); throw e; } catch(Exception e) { getDao().rollbck(); throw e; } finally { getDao().closeSession(); } } ... public FooHibernateDao getDao() { return dao; } public void setDao(FooHibernateDao dao) { this.dao = dao; } }一个类,一个方法相对来说,即使这样处理,只要谨慎一些,还是可以保证程序的正常运行的,可以是,当一个团队中充斥着各色开发人员的时候,当编程规范不能 得到严格执行的时候, 以这样的Hibernate使用方式以及类似结构的程序的设计,对于整个团队来说都不是一件容易的事情。你要关注事务管理,你同时也要关注资源 (Session)的管理,同时还得适度进行异常的处理, 也许每个开发人员都精明能干,但当所有这一切凑在一起的时候,就难免会有疏漏了,这其实也不难解释,以这样的实践来使用HIbernate为什么会时不时 发生资源泄漏之类的事故了。
当然,以上实例只是抽象出来的以分散的Hibernate API封装来使用Hibernate进行数据访问的一种,实际上,只要是同时关注数据访问中的多个方面,并且没有合适的封装方式来使用 Hibernate, 项目中类似于这样的代码应该并不少见,而这直接影响到软件产品的问题,这才是我们应该重点关注的,所以,我们应该寻求新的Hibernate实践,将开发 人员从复杂的数据访问API的使用中解放出来,使其能够更加高效的工作,从而促进整个软件产品的质量提高。
鉴于Hibernate API的使用在具体项目实践过程中也存在类似于Jdbc API使用中资源管理以及异常的处理之类普遍问题, Spring在JdbcTemplate的成功理念之下,对Hibernate的使用以相同的方式进行了封装,从而使得我们不用在资源管理以及异常处理方 面牵扯过多的精力。 同时,Spring对所有的数据访问技术相关的事务管理也通过AOP的形式剥离出去,进一步避免了过多的方面纠缠在一起的困境。
对于旧日的Hibernate操作的封装方式来说,没有能够对Session资源的管理以及数据访问异常进行更为集中合理的处理,所 以,Spring提供了HibernateTemplate对Hibernate的使用进行模板化封装,并且在模板方法内部统一进行数据访问异常的处理。
Session是使用Hibernate与关系数据库进行数据访问的“纽带”,所有的数据 访问操作必须经由Session的支持才能完成, 对于开发来说,我们更多关注的应该是如何使用Session进行数据访问,而至于Session的获取以及释放等资源管理问题则应该尽可能从频繁的数据访 问代码中解脱出去, 以避免过多方面的纠缠。过去的Hibernate的使用,就是因为每一个开发人员都需要同时关注Session资源的管理以及具体的基于Session的 数据访问逻辑, 才会出现稍一不慎就出现资源泄漏的情况。
HibernateTemplate统一对Session的获取以及释放等管理问题进行封装,将Session管理尽量保持在一处管理,而对于不同 的数据访问需求,HibernateTemplate提供了HibernateCallback回调接口以便调用方可以根据各自的数据访问需求进行定制, 完全不会限制Hibernate任何功能的发挥。
与JdbcTemplate的实现相似,HibernateTemplate所有的模板方法以一个主要的模板方法为中心,该方法通过HibernateCallback回调接口为调用方暴露Session资源以进行数据访问,以下是这个主要的模板方法定义:
public Object execute(HibernateCallback action, boolean exposeNativeSession) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Session session = getSession(); boolean existingTransaction = (!isAlwaysUseNewSession() && (!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session, getSessionFactory()))); if (existingTransaction) { logger.debug("Found thread-bound Session for HibernateTemplate"); } FlushMode previousFlushMode = null; try { previousFlushMode = applyFlushMode(session, existingTransaction); enableFilters(session); Session sessionToExpose = (exposeNativeSession ? session : createSessionProxy(session)); Object result = action.doInHibernate(sessionToExpose); flushIfNecessary(session, existingTransaction); return result; } catch (HibernateException ex) { throw convertHibernateAccessException(ex); } catch (SQLException ex) { throw convertJdbcAccessException(ex); } catch (RuntimeException ex) { // Callback code threw application exception... throw ex; } finally { if (existingTransaction) { logger.debug("Not closing pre-bound Hibernate Session after HibernateTemplate"); disableFilters(session); if (previousFlushMode != null) { session.setFlushMode(previousFlushMode); } } else { // Never use deferred close for an explicitly new Session. if (isAlwaysUseNewSession()) { SessionFactoryUtils.closeSession(session); } else { SessionFactoryUtils.closeSessionOrRegisterDeferredClose(session, getSessionFactory()); } } } }因为该模板方法要处理的方面很多,所以,看起来比较复杂,不过,让我们先把事务管理相关的代码先搁置一边,对该模板方法进行简化,这样就比较容易看出该模板方法的庐山真面目了:
public Object execute(HibernateCallback action, boolean exposeNativeSession) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Session session = getSession(); try { Object result = action.doInHibernate(session); return result; } catch (HibernateException ex) { throw convertHibernateAccessException(ex); } catch (SQLException ex) { throw convertJdbcAccessException(ex); } catch (RuntimeException ex) { // Callback code threw application exception... throw ex; } finally { closeSession(session); } }实际上,去除事务相关的代码,为Hibernate操作提供一个模板方法就这么简单,只要将Session资源的管理纳入单一的模板方法,而不是让它任意的散落到代码各处, 我们就可以在很大程度上杜绝Session资源泄漏的危险。
现在如果愿意,你可以在这个主要模板方法的基础上为HibernateTemplate添加更多的数据访问操作,所有之后添加的模板方法最终通过该主要模板方法执行数据访问, 当然,这部分工作spring也已经为我们做了,基本上也不需要我们自己劳神。
在我们简化的HibernateTemplate主要的模板方法中,除了对Session资源管理的代码,还需要我们关注的就是异常的处理。
Hibernate3之前的Hibernate数据访问异常类型是“checked exception”, 自然而然的就会出现本章开头的一幕, 客户端需要根据特定的数据访问方式捕获并处理特定于数据访问技术的异常类型, 所以,Spring从开始对hibernate2提供支持的时候就提供了SessionFactoryUtils工具类以帮助实现从 HibernateException到Spring统一异常体系的转译。
随着越来越多的开发人员对“unchecked exception”在数据访问领域存在合理性的认同,Hibernate3之后的HibernateException也改为了“unchecked exception”类型, 但将“unchecked exception”的HibernateException转译到spring的异常体系依然是值得做的一件事情。
SessionFactoryUtils类提供了convertHibernateAccessException静态方法进行HibernateException到spring异常体系的转译:
public static DataAccessException convertHibernateAccessException(HibernateException ex)HibernateTemplate内部在实现HibernateException的异常转译的时候最终也是委派该方法进行的。 至于模板方法中需要处理的SQLException,HibernateTemplate则将这部分的转译工作转交给了 SQLExceptionTranslator,我想,在经历了JdbcTemplate部分的内容之后,我们对其并不陌生。
如果我们使用HibernateTemplate进行基于Hibernate的数据访问的话,我们可以无需关心具体的异常转译,因为 HibernateTemplate内部可以很好的处理这一点。 不过,即使你不能够使用HibernateTemplate,那也没关系,在你的基于Hibernate数据访问代码中,只要你愿意,依然可以借助 SessionFactoryUtils的convertHibernateAccessException方法进行异常的转译。
HibernateOperations接口定义了能够通过HibernateTemplate使用的所有基于Hibernate的数据访问方 法,HibernateTemplate实现了该接口,不过,HibernateOperations的定义是实在太长, 我们这里就不做罗列的,大家在使用的过程中可以参考HibernateTemplate或者HibernateOperations的javadoc。
要使用HibernateTemplate进行数据访问,只要为其提供一个可以使用的SessionFactory实例即可,通过编程的方式提供还是通过Spring的IoC容器进行注入则完全可以根据具体应用场景来决定。
HibernateTemplate为查询操作提供的模板方法比较丰富,主要分为五组:
-
get模板方法. get组的模板方法可以让你通过指定的主键从数据库中获取相应的对象数据,如果指定的主键没有对应的数据的话,该组方法将返回null,这与Session的get方法的行为是一致的。
-
load模板方法. 与get组模板方法相近,也是通过指定的主键到数据库加载相应的对象数据,但如果指定的主键没有对应的数据的话,该方法将抛出数据访问异常,而不是返回null,与Session中的load方法的行为也是一致的。
-
find模板方法. find组的模板方法还可以细分,包括:
-
find模板方法. 你可以通过find组的模板方法传入相应HQL语句进行查询,该组模板方法将根据指定的HQL语句执行查询,将查询结果以List的形式返回。使用find组模板方法指定的HQL中如果有占位符的话,只能通过“?”指定。
-
findByNamedParam模板方法. 与find模板方法行为相近,只不过,可以指定使用命名参数占位符的HQL语句进行查询。
-
findByNamedQuery模板方法. NamedQuery是指定义于Hibernate的映射文件中的HQL查询定义,findByNamedQuery模板方法对应的HQL中的参数占位符使用“?”。
-
findByNamedQueryAndNamedParam模板方法. 属于findByNamedQuery模板方法补足,可以使用命名参数占位符替代“?”占位符来指定HQL查询语句。
-
-
iterate模板方法. iterate模板方法将按照方法指定的HQL查询语句以及相关参数执行查询,查询结果以迭代器(Iterator)的形式返回,而不是List,当一次 查询数据量比较大,占用系统过多资源的情况下,通常使用iterate模板方法进行查询, 直接使用Session使用iterate方法进行查询也应出于同样目的。
SessionFactory sessionFactory = ...; HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory); Object po = hibernateTemplate.get(FXNewsBean.class,new Integer(1)); if(po == null) // not found else // process information with FXNewsBean要让这段代码运行,前提当然是Hibernate的映射文件以及PO等准备就绪 ,有关如何配置Hibernate进行数据访问请参照Hibernate的参考文档以及相关书籍。
至于其他几组查询方法,留待你去验证和使用吧!
我们这里的更新属于广义上的更新,包括数据的插入,更新和删除操作,HibernateTemplate为这些具体的操作同样提供了完善的模板方法支持:
-
save组模板方法. 包括save和saveOrUpdate两组重载的模板方法,前者会将在本地构建的持久化对象插入数据库,后者则会根据情况来决定是插入还是更新方法参数所提供的持久化对象, 关于save和saveOrUpdate的更多细节,请参阅相关文档。
-
update组模板方法. 对应当前Session的持久化对象如果部分信息修改后,可以通过update方法将修改后的信息更新到后台数据库,以保持持久化对象与数据库数据的一致性。
-
delete组模板方法. 将指定的持久化对象所对应的信息记录清除出后台存储媒介,deleteAll()方法允许你一次指定多个将要进行删除的持久化对象实例。
DataFieldMaxValueIncrementer incrementer = ...; String pk = incrementer.nextStringValue(); FXNewsBean po = new FXNewsBean(); po.setNewsId(pk); po.setNewsTitle("..."); po.setNewsBody("..."); SessionFactory sessionFactory = ...; HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory); hibernateTemplate.save(po); po.po.setNewsBody("new content"); hibernateTemplate.update(po); po.delete(po);
SessionFactory之对于Hibernate,就好象DataSource之对于JDBC,它是所有数据访问资源的发源地,只有获取了 SessionFactory的支持, 后继的数据访问才能继续,这也是为什么HibernateTemplate必须拥有一个SessionFactory才能工作的原因。
要配置并构建一个SessionFactory最简单的方法就是直接通过编程的方式获得:
Configuration cfg = new Configuration() .addResource("mappingResource.hbm.xml") ... .setProperty("hibernate.dialect", "org.hibernate.dialect.MySQLInnoDBDialect") ...; SessionFactory sessionFactory = cfg.buildSessionFactory(); // sessionFactory ready to use不过,我们既然要说的是在Spring中如何配置和获取SessionFactory,那么,这些基本的方式就先放在一边不谈。
Spring采用FactoryBean的形式对SessionFactory的配置和获取进行封装, org.springframework.orm.hibernate3.LocalSessionFactoryBean[10]将 是我们在Spring中配置和获取SessionFactory最为常用的方式。 LocalSessionFactoryBean是其对应的getObject()方法将返回SessionFactory对象的FactoryBean 实现, 你可以通过LocalSessionFactoryBean配置Hibernate数据访问相关的所有资源,包括DataSource,配置文件位置,映 射文件位置甚至于处理LOB数据的LobHandler。
下面是LocalSessionFactoryBean在Spring的IoC容器中的常见配置情况:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${jdbc.url}</value> </property> <property name="driverClassName"> <value>${jdbc.driver}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation"> <value>org.spring21..hibernate.cfg.xml</value> </property> </bean>spring为了能够以统一的方式对各种数据访问方式的事务管理进行抽象,在通过LocalSessionFactoryBean构建 SessionFactory的时候, 使用的是容器内定义的DataSource定义,而不是使用Hibernate内部的ConnectionProvider,实际上,在构建 SessionFactory的实际过程中, spring将根据传入的DataSource来决定为将要构建的SessionFactory提供什么样的ConenctionProvider实现, 这些ConnectionProvider实现包括LocalDataSourceConnectionProvider以及 TransactionAwareDataSourceConnectionProvider, 你可以在LocalSessionFactoryBean同一包下面找到他们。
以上容器配置文件中,我们只通过configLocation就完成了整个的LocalSessionFactoryBean的配置,这是因为通常 的hibernate.cfg.xml配置文件中已经包含了几乎所有必需的配置信息, 包括映射文件资源,各种hibernate配置参数等等。不过,你也可以通过LocalSessionFactoryBean单独指定这些配置项:
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingResources"> <list> <value>cn/spring21/../mapping1.hbm.xml</value> <value>cn/spring21/../mapping2.hbm.xml</value> <value>cn/spring21/../mapping3.hbm.xml</value> <value>...</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> <prop key="...">...</prop> </props> </property> </bean>当然,如果没有特殊目的,我想将所有的hibernate配置信息保存到hibernate指定的配置文件中统一管理才是比较合适的做法。
更多LocalSessionFactoryBean的可配置项请参考javadoc文档。
Hibernate从2.x版本就支持基于xml的映射文件来定义映射信息, 不过,ORM的映射信息可以通过多种方式来表达,随着Java5的普及,越来越多的开发人员喜欢通过Java5中的Annotation来记录一些元数据 信息, Hibernate在这一点上自然也不落后,通过Hibernate Annotation的支持,Hibernate中使用的映射信息现在也可以通过Annotation的形式来表达。 而AnnotationSessionFactoryBean就是为那些通过Annotation获取映射信息的SessionFactory的配置以及 构建而准备的。
AnnotationSessionFactoryBean是在LocalSessionFactoryBean基础之上构建的,除了可以指定 LocalSessionFactoryBean的配置项之外, AnnotationSessionFactoryBean追加定义了几个专门用于获取Annotation元数据定义类的配置项,包括 annotatedClasses,annotatedPackages以及configurationClass。 如下是AnnotationSessionFactoryBean的简单配置实例:
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="dataSource" ref="dataSource"> <property name="annotatedClasses"> <list> <value>cn.spring21..mapping.AnnotationMappingClass1</value> <value>cn.spring21..mapping.AnnotationMappingClass1</value> </list> </property> <property name="annotatedPackages"> <list> <value>cn.spring21..package</value> </list> </property> </bean>
因为标注有映射信息Annotation的类也可以通过在hibernate.cfg.xml之类的统一的配置文件中配置,所以,这种情况下, 也可以不使用annotatedClasses之类的属性,直接使用一个configLocation属性配置即可搞定。
除了可以在本地直接构建SessionFactory实例使用,或者通过Spring的IoC容器注册 LocalSessionFactoryBean或者AnnotationSessionFactoryBean来获取相应的 SessionFactory支持之外, 也可以将SessionFactory绑定到服务器端的JNDI服务上去,当需要使用的时候,可以通过JNDI查询来获得。
但将SessionFactory通过JNDI或者JCA绑定到具体的容器之中,是否必要却有值得商榷的地方。实际上,通过 LocalSessionFactoryBean或者AnnotationSessionFactoryBean在spring容器本地定义并获取 SessionFactory对于通常的应用程序来说是最为合适的方式, 而通过JNDI绑定到具体容器则无法获取任何益处,实际上,SessionFactory所持有的配置信息以及资源很大一部分是应用程序所独有的,比如 Domain Object, 这不同于DataSource,将这些独有的信息绑定到JNDI再去获取,就好象你自己家的电视机必须放到一个地方去保管,当你要看的时候,还需要到那个 地方去申请领回一样。
但对于某些场景来说,通过JCA来注册SessionFactory确实可以获得一定的优势,比如结合EJB使用的时候。
总之,是否要通过JNDI来获取SessionFactory应该根据具体应用程序的场景来决定,但一般情况下,基于Spring的IoC容器的SessionFactory配置及构建就已经是最佳方案了。
与JdbcDaoSupport所完成的使命相同,即使你可以在DAO中直接使用HibernateTemplate进行数据访问,但也没有必要每 个开发人员在各自的每一个DAO实现类中都声明一个HibernateTemplate的实例, 所以,通常我们会提供一个DAO的基类,其中声明有HibernateTemplate的支持,这样,子类只需要使用父类提供的 HibernateTemplate进行数据访问操作即可。
HibernateDaoSupport的出现让我们免于重新去发明这种“基于HibernateTemplate的DAO基类”的轮子,在实现基于HibernateTemplate的DAO实现类的时候, 我们直接继承HibernateDaoSupport就可以获得HibernateTemplate的数据访问支持:
public class FooHibernateDao extends HibernateDaoSupport implements IFooDao { public void insertNews(FXNewsBean newsBean) { getHibernateTemplate().save(newsBean); } public void deleteNews(Object po) { getHibernateTemplate().delete(po); } ... }HibernateDaoSupport还为子类暴露了异常转译的方法:
protected final DataAccessException convertHibernateAccessException(HibernateException ex)即使你在具体的DAO实现类中使用原始的Hibernate API进行数据访问,只要你继承了HibernateDaoSupport,也可以获得HibernateException到spring统一异常体系的转译支持。
iBatis(http://ibatis.apache.org/)是笔者近一年多使用最多的ORM解决方案,如果将Hibernate比做自动 步枪,而Jdbc比做手动步枪的话,那IBatis就得算是半自动步枪了。 iBatis并没有像Hibernate之类的ORM解决方案那样提供完备的ORM特性,包括对象查询语言,透明化持久化等等, 但iBatis却以其他的优势在这百花争艳的ORM武林中占有一席之地。 相对于其他完备的ORM产品来说,IBatis学习曲线很低,你以及你的团队只要精通SQL那基本上就没有太多问题,上手快那绝不是吹出来的,因为笔者亲 身经历过从头带领一个之前没有任何iBatis经验的团队。 如果你想以更加灵活的方式使用JDBC,如果不想引入过于复杂的ORM产品却想使用一定的ORM特性,那么,iBatis应该是你最佳的选择。
不过,既然IBatis也是一种依赖于数据资源的访问技术,那么,通常也避免不了像JDBC以及Hibernate在具体的使用过程中所遇到的资源 管理,异常处理等各种方面的问题, 所以,为了能够在实际的开发过程中使用最佳的iBatis实践方式,先让我们一起来探索一下IBatis最佳实践的前生和今世吧!
在ibatis中,通常是通过com.ibatis.sqlmap.client.SqlMapClient进行数据访问的,当然,在使用之前,我们需要先构建一个可用的实例:
Reader reader = null; SqlMapClient sqlMap = null; try { String resource = "com/ibatis/example/sqlMap-config.xml"; reader = Resources.getResourceAsReader (resource); sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader); } catch (IOException e) { e.printStackTrace(); // don't do this } // sqlMap is ready to use与其他ORM一样,ibatis同样需要一个总的配置文件来获取具体的Sql映射文件,根据该配置文件所提供的配置信息,最终就可以使用 SqlMapClientBuilder来构建一个可用的SqlMapClient实例了。 而这之后,你是通过Singleton方式还是工厂方式来暴露这个实例给系统,那就是“it's up to you”啦!
通常你可以通过三种方式来使用SqlMapClient进行数据访问:
-
基于SqlMapClient的自动提交事务型简单数据访问. 应该说,SqlMapClient对于资源的管理实际上已经进行了足够的封装,使用SqlMapClient进行自动提交事务性质的数据访问,根本就不用考虑太多资源管理的问题:
Map parameters = new HashMap(); parameters.put("parameterName",value); ... Object result = sqlMap.queryForObject("System.getSystemAttribute", parameters);
-
基于SqlMapClient的非自动提交事务型数据访问. 虽然直接使用SqlMapClient进行数据访问可以暂时不考虑资源管理问题,不过在数据访问代码中加入事务控制代码以及异常处理代码之后, 事情看起来就不像开始那么清爽了:
try { sqlMap.startTransaction(); sqlMap.update("mappingStatement"); sqlMap.commitTransaction(); } catch (SQLException e) { e.printStackTrace(); // don't do this } finally { try { sqlMap.endTransaction(); } catch (SQLException e) { e.printStackTrace(); // don't do this } }
-
基于SqlMapSession的数据访问. 另外,你也可以从SqlMapClient中获取SqlMapSession,由自己来管理数据访问资源以及相关的事务控制和异常处理:
SqlMapSession session = null; try { session = sqlMap.openSession(); session.startTransaction(); session.update(""); session.commitTransaction(); } catch (SQLException e) { e.printStackTrace(); // don't do this } finally { if(session != null) { try { session.endTransaction(); } catch (SQLException e) { e.printStackTrace(); // don't do this } session.close(); } }
这种方式可能获取少许的性能优势,不过通常与第二种直接使用SqlMapClient进行数据访问没有太大分别,只不过把一些原来由SqlMapClient管理的任务拿来自己管理而已。
既然有了JDBC和Hibernate集成的经验,我们在ibatis的集成上就开门见山了。因为Spring在将ibatis集成到spring框架的时候,还要考虑将它的事务控制也纳入“spring统一的事务管理抽象层”, 所以,spring使用了基于SqlMapSession的数据访问方式对ibatis进行集成,因为这种方式更为灵活,这样,就可以将ibatis内部可以指定的数据源以及事务管理器等“设备”转由外部提供,比如使用Spring的IoC容器为其注入。
org.springframework.orm.ibatis.SqlMapClientTemplate是Spring为基于ibatis的数 据访问操作提供的模板方法类, 我们可以通过SqlMapClientTemplate结合 org.springframework.orm.ibatis.SqlMapClientCallback回调接口完成所有基于ibatis的数据访问 操作。 SqlMapClientTemplate管理事务,异常处理,资源管理等方面的事情,而SqlMapClientCallback则使得开发人员专注于 具体的数据访问逻辑。
SqlMapClientTemplate中的execute(SqlMapClientCallback)方法是整个SqlMapClientTemplate实现的核心:
public Object execute(SqlMapClientCallback action) throws DataAccessException所有其他的模板方法都是在该模板方法的基础之上为了进一步提供便利而提供的。
execute(SqlMapClientCallback)模板方法以统一的方式对基于ibatis的数据访问操作进行了封装并于一处管理, 可谓集资源管理,异常处理以及事务控制机能于一身:
public Object execute(SqlMapClientCallback action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Assert.notNull(this.sqlMapClient, "No SqlMapClient specified"); // We always needs to use a SqlMapSession, as we need to pass a Spring-managed // Connection (potentially transactional) in. This shouldn't be necessary if // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately // we still need it to make iBATIS batch execution work properly: If iBATIS // doesn't recognize an existing transaction, it automatically executes the // batch for every single statement... SqlMapSession session = this.sqlMapClient.openSession(); if (logger.isDebugEnabled()) { logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation"); } Connection ibatisCon = null; try { Connection springCon = null; DataSource dataSource = getDataSource(); boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy); // Obtain JDBC Connection to operate on... try { ibatisCon = session.getCurrentConnection(); if (ibatisCon == null) { springCon = (transactionAware ? dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource)); session.setUserConnection(springCon); if (logger.isDebugEnabled()) { logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation"); } } else { if (logger.isDebugEnabled()) { logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation"); } } } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } // Execute given callback... try { return action.doInSqlMapClient(session); } catch (SQLException ex) { throw getExceptionTranslator().translate("SqlMapClient operation", null, ex); } finally { try { if (springCon != null) { if (transactionAware) { springCon.close(); } else { DataSourceUtils.doReleaseConnection(springCon, dataSource); } } } catch (Throwable ex) { logger.debug("Could not close JDBC Connection", ex); } } // Processing finished - potentially session still to be closed. } finally { // Only close SqlMapSession if we know we've actually opened it // at the present level. if (ibatisCon == null) { session.close(); } } }该方法定义初看起来或许感觉繁杂,实际上,只不过是因为spring在集成ibatis的时候要考虑在整个框架内以统一的方式处理事务管理才会出现这初看起来不慎清晰的代码实现。
-
execute方法一开始通过给定的sqlMapClient获取相应的SqlMapSession:
SqlMapSession session = this.sqlMapClient.openSession();
我说过spring在集成ibatis的时候使用的是基于SqlMapSession的数据访问方式,所以,这行代码很好理解; -
从Connection ibatisCon = null;这行代码一直到// Execute given callback...这行注释之前, 你都可以看作是spring在处理对事务管理的集成,spring会根据当前的事务设置来决定通过何种方式从指定的dataSource中获取相应的Connection供SqlMapSession使用;
-
之后的代码就比较容易理解了,模板方法会调用SqlMapClientCallback的回调方法来进行数据访问,如果期间出现 SQLException,则通过提供的SQLExceptionTranslator进行异常转译, 最后,合适的关闭使用的数据库连接和SqlMapSession完事。
为了避免开发人员在一些通用的数据访问操作上提供几乎相同的SqlMapClientCallback实现, SqlMapClientTemplate同时在execute(SqlMapClientCallback)核心模板方法的基础上提供了其他便利的数据 访问模板方法,而这些模板方法在实现数据访问逻辑的时候,最终都会回归核心模板方法的“怀抱”:
public Object queryForObject(final String statementName, final Object parameterObject) throws DataAccessException { return execute(new SqlMapClientCallback() { public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { return executor.queryForObject(statementName, parameterObject); } }); }到现在,您是不是应该对SqlMapClientTemplate更加了解了那?
SqlMapClientTemplate底层依赖于com.ibatis.sqlmap.client.SqlMapClient提供基于 ibatis的数据访问支持, 所以,要使用SqlMapClientTemplate,我们首先最少需要提供一个SqlMapClient。
如果你是通过编程的方式使用SqlMapClientTemplate,那么你就可以像当初那样,直接使用ibatis的方式,使用SqlMapClientBuilder构建相应的SqlMapClient实例然后传给SqlMapClientTemplate:
String resource = "com/ibatis/example/sqlMap-config.xml"; Reader reader = Resources.getResourceAsReader (resource); SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader); SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(sqlMap); // sqlMapClientTemplate is ready to use当然,只提供SqlMapClient实例的话,SqlMapClientTemplate将使用ibatis配置文件内部指定的dataSource,我们也可以使用外部定义的DataSource:
// 1. datasource definition BasicDataSource dataSource = new BasicDataSource(); ... // 2. SqlMapClient definition ... SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader); // 3. construct the SqlMapClientTemplate SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(dataSource,sqlMap); // sqlMapClientTemplate is ready to use
实际上,除了直接通过编程的方式配置和使用SqlMapClientTemplate,更多时候我们是通过IoC容器来配置和使用 SqlMapClientTemplate的, 在spring的IoC容器中,我们使用 org.springframework.orm.ibatis.SqlMapClientFactoryBean来配置和获取相应的 SqlMapClient,并注入给SqlMapClientTemplate使用。 SqlMapClientFactoryBean也是一个FactoryBean实现,其getObject()方法返回的当然就是我们需要的 SqlMapClient啦。
通过SqlMapClientFactoryBean配置和使用SqlMapClient以及SqlMapClientTemplate情形如下:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${db.main.url}</value> </property> <property name="driverClassName"> <value>${db.main.driver}</value> </property> <property name="username"> <value>${db.main.user}</value> </property> <property name="password"> <value>${db.main.password}</value> </property> ... </bean> <bean id="mainSqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate"> <property name="sqlMapClient"> <bean class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <property name="dataSource"> <ref bean="mainDataSource"/> </property> <property name="configLocation"> <value>file:conf/ibatis/ibatis-config.xml</value> </property> </bean> </property> </bean>如果容器中的SqlMapClientFactoryBean仅限于一个SqlMapClientTemplate使用的话,可以像如上配置那样将SqlMapClientFactoryBean配置为“inner bean”的形式, 当然,这不是必须的。
SqlMapClientCallback在整个spring对ibatis的集成中所处的地位仅次于SqlMapClientTemplate, 通过SqlMapClientTemplate的execute(SqlMapClientCallback)模板方法结合 SqlMapClientCallback我们可以可以完成任何基于ibatis的数据访问操作。
SqlMapClientCallback的定义很简单(通常Callback接口都很简单),通过doInSqlMapClient()方法暴露了com.ibatis.sqlmap.client.SqlMapExecutor用于具体的数据访问操作:
Object doInSqlMapClient(com.ibatis.sqlmap.client.SqlMapExecutor executor) throws SQLException有了SqlMapExecutor,你几乎就可以在ibatis的世界里面为所欲为了,包括基本的查询/插入/更新/删除等基本数据访问操作,甚至于批量更新操作。
如果我们要向某一个数据库表批量更新数据的话,我们可以借助于SqlMapClientCallback所暴露的SqlMapExecutor轻而易举的完成:
protected void batchInsert(final List beans) { sqlMapClientTemplate.execute(new SqlMapClientCallback() { public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException { executor.startBatch(); Iterator iter = beans.iterator(); while (iter.hasNext()) { YourBean bean = (YourBean)iter.next(); executor.insert("Namespace.insertStatement", bean); } executor.executeBatch(); return null; } }); }至于其他基于ibatis的基本的数据访问操作,也可以类似的方式为SqlMapClientTemplate提供相应的 SqlMapClientCallback实现, 不过,通常情况下,我们不需要直接如此,因为SqlMapClientTemplate为我们考虑的更多。
如果你要使用SqlMapClientTemplate进行基于ibatis的基本的一些数据访问操作,那么,实际上你不需要每次都提供一个 SqlMapClientCallback实现,然后通过execute(SqlMapClientCallback)来执行这些操作, 为了便利起见,SqlMapClientTemplate为这些基本数据操作提供了足以满足你需要的多组模板方法,你所要做的,仅仅是根据需要来选用这些 便利的模板方法而已。
所有的SqlMapClientTemplate中定义的数据访问操作方法都在 org.springframework.orm.ibatis.SqlMapClientOperations接口中定义, 当然也包括这些便利的模板方法,实际上,开发过程中,借助于IDE的支持,只要稍微参照一下javadoc就能娴熟的使用这些模板方法:
SqlMapClientTemplate sqlMapClientTemplate = ...; Object parameter = ...; // 1. insert sqlMapClientTemplate.insert("insertStatementName",parameter); // 2. update int rowAffected = sqlMapClientTemplate.update("updateStatementName"); // 3. delete int rowAffected = sqlMapClientTemplate.delete("deleteStatementName",parameter); // 4. query Object result = sqlMapClientTemplate.queryForObject("selectStatementName"); List resultList = sqlMapClientTemplate.queryForList("selectStatementName"); Map resultMap = sqlMapClientTemplate.queryForMap("selectStatementName"); sqlMapClientTemplate.queryWithRowHandler("hugeSelect", new RowHandler(){ public void handleRow(Object valueObject) { ResultBean bean = (ResultBean)valueObject; process(bean); }});更多信息可以参照SqlMapClientTemplate的Javadoc和ibatis的参考文档以及相关书籍。
与集成其他数据访问方式同等待遇,如果要使用ibatis实现相应的DAO的话,Spring为我们提供了 org.springframework.orm.ibatis.support.SqlMapClientDaoSupport作为整个DAO层次体系 的基类, 所有开发人员在使用ibatis进行DAO开发的时候,直接继承SqlMapClientDaoSupport类即可获得 SqlMapClientTemplate的数据访问支持:
public class FooSqlMapClientDao extends SqlMapClientDaoSupport implements IFooDao { public void update(YourBean bean) { getSqlMapClientTemplate.update("updateStatementName",bean); } public Object get(String pk) { return getSqlMapClientTemplate().queryForObject("selectStmtName",pk); } ... }当然,如果你愿意,直接为相应的DAO注入SqlMapClientTemplate来使用也没有人会反对,尤其是当系统中旧有的DAO基类已经存在的情况下。
不管是从对各种ORM产品的集成理论以及集成方式上看,还是从对各种ORM产品集成关注点来看, Spring对各种ORM产品的集成几乎是一脉相承的,所以,对于Spring中其他几种ORM产品的集成情况, 没有必要再重复几乎一样的理论和方式,故此,以下内容仅作提点,不着更多笔墨,我想各位也应该能够理解。
回顾spring对各种数据访问技术的集成,我们可以归纳几点如下:
-
集成理论和方式上. 各种数据访问方式的管理和使用通过相应的模板方法类来统一建模,而具体的数据访问逻辑则统一由相应的回调接口提供, 从而将数据访问资源的管理和具体的数据访问逻辑相分离。
-
集成关注点上. spring对各种数据访问技术的集成主要在三个主要的关注点上:
-
数据访问资源管理,主要设计两种管理对象:
-
连接工厂(ConnectionFactory). 连接工厂代表的是创建数据访问会话资源的统一概念,通常我们可以通过特定的数据访问技术的支持来直接创建它们,也可以通过JNDI等服务获取已经创建并配 置好的实例。 对于JDBC来说,对应连接工厂概念的实体是DataSource,Hibernate是SessionFactory,iBatis是 SqlMapClient...
-
连接(Connection)或者说会话资源. 连接(Connection)是客户端与数据媒介进行数据通信的纽带,每次进行数据访问的时候都需要从指定的连接工厂获得某个连接 (Connection)以完成本次数据访问操作, 操作完成后关闭当前连接资源。对于JDBC来说,连接概念对应的是java.sql.Connection,对于Hibernate来说,对应的是 Session,对于ibatis是SqlMapSession...
-
-
特定数据访问异常的转译,将这些特定的数据异常转译为spring统一的数据访问异常体系,从而使得客户端可以使用统一的方式透明的处理数据访问异常;
-
将特定于数据访问技术的事务管理,统一纳入到spring的事务管理抽象层,使得我们可以统一的方式来管理事务,最主要的,通过spring的事务管理抽象层,我们可以得到EJB2时代只有通过相应的EJB Container才能获得的声明型事务支持。
-
1.3.3.1. spring对JDO[11]的集成
JDO(http://java.sun.com/jdo/)是Sun提出来的数据持久化规范,该规范从1.0版本发展到现在的2.0版本,虽然整个发展历程不如Hibernae那样突飞猛进, 但也涌现了不少优秀的JDO产品实现,比如KodoJDO,JPOX(http://www.jpox.org/)等,现在JDO规范的发展主要有Apache JDO(http://db.apache.org/jdo/index.html)开源项目来推动。
JDO中对应数据访问ConnectionFactory概念的API为PersistenceManagerFactory,在spring中, 你可以直接实例化特定的JDO实现产品提供的PersistenceManagerFactory 实现类,或者通过org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean来配 置和创建PersistenceManagerFactory:
-
使用JDO产品实现类直接创建PersistenceManagerFactory实例. 要使用DataSource,你可以直接实例化Jakarta Commons DBCP的BasicDataSource,也可以直接实例化C3P0的ComboPooledDataSource,对于 PersistenceManagerFactory来说, 我们也可以像实例化DataSource一样,直接实例化相应JDO产品提供的PersistenceManagerFactory实现类:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${db.main.url}</value> </property> <property name="driverClassName"> <value>${db.main.driver}</value> </property> <property name="username"> <value>${db.main.user}</value> </property> <property name="password"> <value>${db.main.password}</value> </property> ... </bean> <bean id="persistenceManagerFactory" class="org.jpox.PersistenceManagerFactoryImpl" destroy-method="close"> <property name="connectionFactory" ref="mainDataSource"/> <property name="nontransactionalRead" value="true"/> </bean>
以上我们在IoC容器中注册了开源JDO实现JPOX的PersistenceManagerFactory实现类(当然,如果你的应用不使用IoC容器,直接编程实例化即可)。 -
通过LocalPersistenceManagerFactoryBean创建PersistenceManagerFactory实例. 为便于在容器中配置和管理PersistenceManagerFactory,spring提供了针对 PersistenceManagerFactory的FactoryBean实现 LocalPersistenceManagerFactoryBean, 通过它,我们可以在容器中创建PersistenceManagerFactory实例并于整个的容器上下文中共享:
<bean id="persistenceManagerFactory" class="org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean"> <property name="configLocation" value="classpath:jdo-config.properties"/> </bean>
如果你愿意,可以将该实例注入容器中任何依赖于PersistenceManagerFactory的bean定义。
JdoTemplate就是spring为JDO提供的模板方法类,用来统一管理数据访问资源,异常处理以及事务控制,它的核心模板方法 execute(JdoCallback)通过org.springframework.orm.jdo.JdoCallback回调接口提供具体的数据 访问逻辑。 JdoCallback接口的定义如下:
Object doInJdo(javax.jdo.PersistenceManager pm)该回调接口为开发人员暴露了PersistenceManager以进行基于JDO的数据访问操作。
JdoTemplate除了提供了该核心模板方法之外,为了简化较为普遍的数据访问操作,同时在核心模板方法基础之上提供了多组便利的模板方法,所 有这些模板方法定义,你可以通过org.springframework.orm.jdo.JdoOperations接口定义获得, 因为JdoTemplate本身就是实现了该接口以便为开发人员提供统一的数据访问接口。
如果你直接使用JdoTemplate的话,那么有关JDOException到Spring异常体系的转译将会由JdoTemplate为你处 理,你无需关心特定于JDO的异常转译; 不过,即使你直接使用JDO的API进行数据访问的话(没有发现现在各个ORM产品在处理资源管理方面的API设计改进很多吗?), 通过org.springframework.orm.jdo.PersistenceManagerFactoryUtils工具类提供的静态方法 convertJdoAccessException(), 你依然可以获得从JDOException到spring统一异常层次体系的转译支持:
public static DataAccessException convertJdoAccessException(JDOException ex)另外,你也可以通过PersistenceManagerFactoryUtils的newJdbcExceptionTranslator方法获得SQLExceptionTranslator的相应实例来处理数据访问期间更加具体的SQLException:
SQLExceptionTranslator sqlExTranslator = PersistenceManagerFactoryUtils.newJdbcExceptionTranslator(dataSource); if(ex.getCause() instanceof SQLException) return sqlExTranslator.translate("task","sql",ex); ...
spring为所有基于JDO的DAO实现类提供了 org.springframework.orm.jdo.support.JdoDaoSupport作为继承的基类, 所有继承了JdoDaoSupport的DAO实现类将直接获得JdoTemplate的数据访问支持:
public class FooJdoDaoImpl extends JdoDaoSupport implements IFooDao { public void delete(Object po) { getJdoTemplate().deletePersistent(po); } ... }当然,每一个DAO使用前,你需要为他们注入一个JdoTemplate实例或者一个PersistenceManagerFactory实例, 如果注入的是后者,JdoDaoSupport内部将根据提供的PersistenceManagerFactory构建相应的JdoTemplate使 用。
使用JdoDaoSupport的好处是,整个团队可以统一的DAO基类作为基础进行开发,JdoDaoSupport本身已经提供了最基本的基于 JDO的数据访问支持, 而且,如果你愿意,你可以直接使用基类提供的PersistenceManagerFactory使用JDO原始API,当然,大多数情况下这是没有必要 的。
如果系统之前就已经存在DAO基类的话,扩展JdoDaoSupport显然是不现实的,这个时候你可以直接为DAO注入JdoTemplate, 或者重新设立一套Dao实现体系, 但是,对于后者来说,往往是比较危险的举动,毕竟,一个团队内部的开发需要保持一致性,重新设立一套实现体系在技术层次上当然是比较合适的,但从管理和维 护的角度,则确实有值得商榷的地方。
TopLink的命运比较曲折,作为最初由Object People开发的几乎是Java界ORM产品鼻祖的Toplink, 在上世纪90年代末被短命的WebGain[12]收购之后,没过多久又被Oracle收购,并一直跟随Oracle走到今天,然后于2007年被Oracle开源, 但不管怎么说,最为一个商业产品,Toplink还是比较令人称道的。
Toplink中没有明确的实现类对应“数据访问ConnectionFactory”的 概念,所以,为了能够以统一的方式集成toplink, spring对toplink中的ServerSession进行了进一步的抽象, 使用org.springframework.orm.toplink.SessionFactory作为spring中toplink数据访问所对应的 “数据访问ConnectionFactory”。
对于org.springframework.orm.toplink.SessionFactory的配置和创建当然又是老生常谈 啦,spring通过org.springframework.orm.toplink.LocalSessionFactoryBean这个 FactoryBean对org.springframework.orm.toplink.SessionFactory的配置和创建进行封装, 这样,我们就可以在容器中添加如下的bean定义以使用:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${db.main.url}</value> </property> <property name="driverClassName"> <value>${db.main.driver}</value> </property> <property name="username"> <value>${db.main.user}</value> </property> <property name="password"> <value>${db.main.password}</value> </property> ... </bean> <bean id="mySessionFactory" class="org.springframework.orm.toplink.LocalSessionFactoryBean"> <property name="configLocation" value="toplink-sessions-config.xml"/> <property name="dataSource" ref="mainDataSource"/> </bean>如果你要通过编程的方式来实例化相应的org.springframework.orm.toplink.SessionFactory的话,也可以直接 使用org.springframework.orm.toplink.LocalSessionFactory, LocalSessionFactoryBean是为IoC容器准备的,LocalSessionFactory则更多是为直接编程实例化准备的。
org.springframework.orm.toplink.TopLinkTemplate是spring为基于toplink的数据访问提供的模板方法类, 其核心模板方法定义如下:
public Object execute(TopLinkCallback action) throws DataAccessExceptionorg.springframework.orm.toplink.TopLinkCallback是该核心模板方法所使用的回调接口,开发人员可以通过该回调接口提供具体的数据访问逻辑实现:
public interface TopLinkCallback { Object doInTopLink(Session session) throws TopLinkException; }TopLinkCallback为开发人员暴露了Session资源以供数据访问的使用,而对于该资源的获取和释放则由TopLinkTemplate统一管理。
按照惯例,TopLinkTemplate在提供核心模板方法以满足一般数据访问需求的情况下,还会在该核心模板方法的基础上提供更多便于基本数据 访问操作的模板方法, 所有这些模板方法全部由org.springframework.orm.toplink.TopLinkOperations接口来定 义,TopLinkTemplate实现了该接口,所以提供了所有这些模板方法的实现以供我们使用。
如果我们直接使用TopLinkTemplate进行数据访问,那么我们无需关心特定的数据访问异常,比如SQLException以及 TopLinkException到spring异常体系的转译, 因为TopLinkTemplate内部已经帮助我们做了这些工作。
不过,即使我们不通过TopLinkTemplate进行基于Toplink的数据访问,而是直接使用toplink的原始api进行数据访问的 话, 我们依然能够获得TopLinkException到spring异常体系的转译支持,只需要在处理异常的时候通过 org.springframework.orm.toplink.SessionFactoryUtils的静态方法 convertTopLinkAccessException进行异常转译即可:
public static DataAccessException convertTopLinkAccessException(TopLinkException ex)当然,无论如何,通过TopLinkTemplate进行基于toplink的数据访问通常才是比较合适的方式。
spring为使用基于toplink的数据访问技术实现的所有DAO提供了 org.springframework.orm.toplink.support.TopLinkDaoSupport作为整个DAO实现体系的顶层基 类, 所有基于toplink的数据访问DAO实现类直接继承TopLinkDaoSupport就可获得TopLinkTemplate或者Toplink数 据访问API的支持:
public class FooToplinkDaoImpl extends TopLinkDaoSupport implements IFooDao { public Object find(String pk) { return getTopLinkTemplate().readById(Foo.class,pk); } ... }除了提供TopLinkTemplate以进行基于toplink的数据访问,你还可以通过TopLinkDaoSupport获取toplink的原始 API进行数据访问,同时,TopLinkDaoSupport还提供了相应的异常转译方法, 进一步提升了TopLinkDaoSupport作为DAO基类的存在价值。
1.3.3.3. spring对JPA[13]的集成
JPA是Sun于JavaEE5之后提出的ORM解决方案的统一标准,具体实现由不同提供商提供,包括hibernate,toplink等,给我感觉,就好象当年的JDBC标准一样,呵呵,只不过,JPA是面向ORM的统一。
Spring框架于2.0版本之后提供了对JPA的支持,并且在之后的版本中将做进一步的统一和完善。
JPA中的EntityManagerFactory对应“数据访问的ConnectionFactory”的概念, spring提供了三种方式来配置和获取EntityManagerFactory:
-
使用LocalEntityManagerFactoryBean. 对于基于JavaSE的独立应用程序或者测试环境下的开发,我们可以使用 org.springframework.orm.jpa.LocalEntityManagerFactoryBean来获取相应的 EntityManagerFactory, LocalEntityManagerFactoryBean将默认读取META-INF/persistence.xml配置文件信息来构建相应的 EntityManagerFactory,我们只需要指定相应的persistenceUnit名称即可:
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"> <property name="persistenceUnitName" value="yourPersistenceUnitName"/> </bean>
这种方式应用场景有限,对于更多定制需求,可以考虑使用LocalContainerEntityManagerFactoryBean。 -
使用LocalContainerEntityManagerFactoryBean . org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean是比较常用的 配置和获取EntityManagerFactory的方式,通过它, 我们可以指定自定义的配置文件位置,独立的dataSource定义甚至spring提供的定制字节码转换的loadTimeWeaver实现:
<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url"> <value>${db.main.url}</value> </property> <property name="driverClassName"> <value>${db.main.driver}</value> </property> <property name="username"> <value>${db.main.user}</value> </property> <property name="password"> <value>${db.main.password}</value> </property> ... </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="mainDataSource"/> <property name="loadTimeWeaver"> <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/> </property> <property name="persistenceXmlLocation" value="META-INF/main-persistence.xml"/> </bean>
-
通过JNDI获取EntityManagerFactory. 我们也可以使用绑定到JNDI的EntityManagerFactory,对于基于Spring的ioc容器的应用来说,仅仅也就是配置文件的少许改动:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>persistence/pUnitName</value> </property> </bean>
当然,spring2.0之后,我们更喜欢用XSD风格的配置:<jee:jndi-lookup id="entityManagerFactory" jndi-name="persistence/pUnitName"/>
相对来说,这种方式更加简洁,描述性也更强。
获取EntityManagerFactory实例之后,我们就可以使用spring为JPA提供的模板方法类org.springframework.orm.jpa.JpaTemplate进行数据访问了。 JpaTemplate的核心模板方法为“Object execute(JpaCallback action)”,开发人员直接通过org.springframework.orm.jpa.JpaCallback回调接口提供具体的数据访问逻辑即可, 至于说资源管理,事务以及异常处理等关注点,则由JpaTemplate模板方法来统一管理。
JpaCallback接口定义为开发人员曝露了EntityManager实例用于具体的基于JPA的数据访问操作:
public interface JpaCallback { Object doInJpa(EntityManager em) throws PersistenceException; }通过EntityManager,开发人员可以完成任何基于JPA原始API可以完成的工作,而至于说EntityManager获取以及释放的管理问题,则由JpaTemplate为我们操心就行了。
org.springframework.orm.jpa.JpaOperations接口定义了所有JpaTemplate中可用的数据访问模板 方法,除了核心的execute(JpaCallback)模板方法, 还有一些便于常用操作的模板方法,具体可以参阅JpaOperations或者JpaTemplate的javadoc。
JpaTemplate模板方法内部同时处理了JPA的数据访问异常到spring统一异常体系的转译,所以,如果我们使用JpaTemplate进行数据访问的话, 对于特定于JPA的数据访问异常的处理,我们可以不用关心。
不过即使我们在自己的DAO实现中没有使用JpaTemplate,而是使用JPA原始API进行数据访问,我们依然能够得到spring统一异常 体系的好处, JpaTemplate内部的异常转译实际上最终是通过 org.springframework.orm.jpa.EntityManagerFactoryUtils提供的静态方法 convertJpaAccessExceptionIfPossible()完成的:
public static DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex)如果需要,我们同样可以直接使用该工具类提供的这个方法来完成到spring统一异常体系的转译。
spring为所有基于Jpa进行数据访问的DAO实现类提供了一个统一的基类,即 org.springframework.orm.jpa.support.JpaDaoSupport, 相应的DAO实现类可以直接继承JpaDaoSupport以获得JpaTemplate提供的Jpa数据访问支持。
当然,JpaDaoSupport允许我们注入EntityManagerFactory 或者EntityManager 实例,如果愿意,你也可以直接使用JPA的原始API进行数据访问。
纵观整个“Spring数据访问支持”一章内容,我们会发现,不管是Spring对JDBC API的抽象还是对Hibernate,IBatis等ORM的集成, 全部都是采用一种理念或者处理方式进行的,那就是“模板方法模式” + 相应的Callback接口。
那么,为什么要在这里使用模板方法 + Callback的问题处理方式那?实际上,最基本的问题在于:不管是jdbc还是hibernate或者其他ORM实现,在资源管理上有一个通用的问 题,那就是需要在资源使用之后可以安全的释放这些资源。 与《Bitter Java》所提出的理念相同,为了确保尽可能的将资源的获取和资源的释放操作放在一起,spring在数据访问层处理资源的问题上,采用了 Template Method Pattern。 这样,以一种统一集中的方式来处理资源获取和释放,避免了将这种容易出现问题的操作分散于代码中的各个地方,进而也就避免了由此产生的其他资源泄漏一类比 较严重的问题。
推而广之,我们可以以相同的模式来处理类似的问题,而我们也会发现,这样的处理与我们之前的处理或者封装方式是如此的不同,如此的简洁明了。
之前我们说过,通常的FX系统会从相应的新闻提供商那里定期获取外汇交易相关新闻,最常见的方式就是通过FTP协议到指定的FTP服务器去定期下载 相应的新闻文件, 所以,FX系统的应用程序需要提供相应的实现类来进行FTP操作。而程序中的FTP操作应该是比较通用的,无非就是上传下载文件之类,为了程序能有一个良 好的结构,我们通常会将这些FTP操作逻辑封装为一个工具类。 而下面我们将看到的,就是两种截然不同的工具类实现方式。
最为底层的FTP操作我们不需要重新发明轮子,Jakarta Commons Net类库提供了基本的FTP支持,不过,直接使用Commons Net的API就跟直接使用Jdbc API一样让人尴尬(以下代码摘自FTPClient的Javadoc):
boolean error = false; try { int reply; ftp.connect("ftp.foobar.com"); System.out.println("Connected to " + server + "."); System.out.print(ftp.getReplyString()); // After connection attempt, you should check the reply code to verify // success. reply = ftp.getReplyCode(); if(!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); System.err.println("FTP server refused connection."); System.exit(1); } ... // transfer files ftp.logout(); } catch(IOException e) { error = true; e.printStackTrace(); } finally { if(ftp.isConnected()) { try { ftp.disconnect(); } catch(IOException ioe) { // do nothing } } System.exit(error ? 1 : 0); }OK,我得承认,FTPClient类提供的这段代码只是一段实例,也不想我们在实际的生产环境下使用它,所以,我们尝试对其进行封装。
对于使用FTPClient类实现FTP操作来说,无非就是登陆FTP服务器,传输文件,然后退出服务器三步,所以,如下的FTP操作工具类也是最为常见的实现方式:
class Phase1FtpUtility { public boolean login(...) { ... // login code } public void doTransfer(...) { ...// your transfer logic } public boolean logout() { ...// logout } }相对于实例中的代码来说,通过Phase1FtpUtility类的封装,现在看起来进行FTP操作要简洁多了,不过,这样的封装方式并没有起到多少实际效果:
-
Phase1FtpUtility对FTPClient API的封装力度不够,与直接使用FTPClient的API相比,调用方也仅仅是少写几行代码而已:
Phase1FtpUtility ftpUtility = ...; if(ftpUtility.login(..)) { ftpUtility.doTransfer(..); } ftpUtiligy.logout();
而且,你把资源的管理下放给了每一处调用Phase1FtpUtility进行ftp操作的调用代码,就跟数据库连接一样,你又能如何保证相应的资源在每 一处都获得释放那? 我想,就现有的API封装方式,我们只能加强开发人员的约束力来达到正确使用API的目的了。 -
Phase1FtpUtility的doTransfer(..)方法通常用来实现具体的ftp操作逻辑,那么,现在的 Phase1FtpUtility只能提供固定的ftp操作逻辑,如果其他调用方需要不同的ftp操作, 那么,或许得子类化Phase1FtpUtility并覆写(Override)doTransfer(..)方法了,不过,这样好像偏离了我们要将 Phase1FtpUtility作为单一工具类进行使用的初衷。
public class FTPClientTemplate { // private static final Log logger = LogFactory.getLog(FTPClientTemplate.class); // private FTPClientConfig ftpClientConfig;// optional // private String server;// required private String username; // required private String password; // required private int port=21;// optional public FTPClientTemplate(String host,String username,String password) { this.server = host; this.username = username; this.password = password; } /** * the common interface method for ftp operations, only refer to this method if other methods don't meed your need.<br> * with your own FtpTransferCallback implementation, you can do almost everything that you can do with FTPClient.<br> * * @param callback The FtpTransferCallback instance * @throws IOException some thing goes wrong while ftp operations. */ public void execute(FTPClientCallback callback) throws IOException { FTPClient ftp = new FTPClient(); try { if(this.getFtpClientConfig() != null) ftp.configure(this.getFtpClientConfig()); ftp.connect(server,getPort()); // check whether the connection to server is confirmed int reply = ftp.getReplyCode(); if(!FTPReply.isPositiveCompletion(reply)) { throw new IOException("failed to connect to the FTP Server:"+server); } // login boolean isLoginSuc= ftp.login(this.getUsername(),this.getPassword()); if(!isLoginSuc) { throw new IOException("wrong username or password,please try to login again."); } // do your ftp operations in call back method callback.processFTPRequest(ftp); // logout ftp.logout(); } finally { if(ftp.isConnected()) { ftp.disconnect(); } } } /** * * @param path the directory that contains the files to be listed. * @return file names contained in the "remoteDir" directory * @throws IOException some thing goes wrong while ftp operations. */ public String[] listFileNames(final String remoteDir,final String fileNamePattern) throws IOException { final List<String[]> container = new ArrayList<String[]>(); execute(new FTPClientCallback(){ public void processFTPRequest(FTPClient ftp) throws IOException { ftp.enterLocalPassiveMode(); changeWorkingDir(ftp,remoteDir); if(logger.isDebugEnabled()) logger.debug("working dir:"+ftp.printWorkingDirectory()); container.add(ftp.listNames(fileNamePattern)); }}); return container.get(0); } protected void changeWorkingDir(FTPClient ftp, String remoteDir) throws IOException { Validate.notEmpty(remoteDir); ftp.changeWorkingDirectory(remoteDir); } ... // setters and getters ... }我们通过execute(FTPClientCallback)方法对整个的基于FTPClient的API使用流程进行了封装,而将我们真正关心的每次具体的FTP操作交给了FTPClientCallback:
public interface FTPClientCallback { public void processFTPRequest(FTPClient ftpClient) throws IOException; }现在我们要做的,实际上就是根据每次FTP操作请求细节提供相应的FTPClientCallback实现给FTPClientTemplate执行即可:
FTPClientTemplate ftpTemplate = new FTPClientTemplate(host,user,pwd); FTPClientCallback callback = new FTPClientCallback() { public void processFTPRequest(FTPClient ftpClient) throws IOException { ... // your ftp operations } }; ftpTemplate.execute(callback);FTPClientTemplate一旦构建完成,其他任何调用方都可以共享使用它,只要调用方每次提供自己的FTPClientCallback实现即可。
对于现在的FTPClientTemplate来说,看起来可能过于单薄,对于某些常用的FTP操作,像文件上传,文件下载,文件列表读取等等, 我们可以在FTPClientTemplate内直接提供,而没有必要让调用方每次实现几乎相同的代码。 listFileNames(remoteDir,fileNamePattern)方法就是这样的方法实现, 实际上,无非就是提供了相应的FTPClientCallback实现,然后最终委托execute()方法执行而已。
作为工具类,我们可以直接将这些常用的FTP操作方法定义到FTPClientTemplate之中,不过,如果你愿意,也可以设计一个FTPOperation之类的接口, 里面定义一系列的FTP操作方法,然后让FTPClientTemplate来实现该接口。
现在基于REST方式的Web Service好像较之原来SOAP的方式更加受人欢迎一些,而世界上许多券商或者银行通常也会以REST的方式发送一些外汇牌价之类的信息, 甚至,FX系统的某些外汇新闻提供商也通过HTTP协议采用类似于REST的方式来发送新闻,那么,与基于FTP协议的信息交换类似,对于这种方式的信息 交换,我们也需要在应用程序中采用适当的API进行处理。
Apache Commons HttpClient是一个提供HTTP协议支持的Java类库,许多应用包括稍后我们将提到的Spring的Remoting支持都是采用该类库实现的, 我们同样可以使用该类库进行基于HTTP协议的信息交换,或者说得更“时髦”一点儿,进行REST方式的WebService开发。
如果你是初次接触HttpClient,那么你一定应该先看一下HttpClient网站提供的Tutorial文档,里面给出了类似如下的使用代码示例:
public class HttpClientTutorial { private static String url = "http://www.apache.org/"; public static void main(String[] args) { // Create an instance of HttpClient. HttpClient client = new HttpClient(); // Create a method instance. GetMethod method = new GetMethod(url); // Provide custom retry handler is necessary method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false)); try { // Execute the method. int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) { System.err.println("Method failed: " + method.getStatusLine()); } // Read the response body. byte[] responseBody = method.getResponseBody(); // Deal with the response. // Use caution: ensure correct character encoding and is not binary data System.out.println(new String(responseBody)); } catch (HttpException e) { System.err.println("Fatal protocol violation: " + e.getMessage()); e.printStackTrace(); } catch (IOException e) { System.err.println("Fatal transport error: " + e.getMessage()); e.printStackTrace(); } finally { // Release the connection. method.releaseConnection(); } } }又是获取资源,操作,释放资源,处理异常等等,而且这样的代码无法用于生产环境也是肯定的了,那该怎么处理我想你已经心里有数了吧?!余下的部分,还是留给你来实现它吧!
对于一个项目来说,数据访问方式的选择通常并非仅从技术层面考虑就可以确定的, 政治层面的考虑,经济层面的考虑可能都需要考虑进去,不过这些其实跟我们开发人员没有太多关系,所以,我们这里撇看其他层面的问题先不谈, 仅从技术层面来谈谈各种数据访问方式的选择。
要说经历过现在Java平台所有数据访问技术的“洗礼”,这多少有些为难笔者了,毕竟,要真的在全国甚至全世界都挑出几个精通现在流行的多种数据访问技术的人物好像也不是那么容易吧! 所以,笔者这里只能以较为常见并且笔者也曾使用过的数据访问技术为例,简单谈一些可能并不成熟的看法。
我们将以JDBC,iBatis和Hibernate为讨论的中心,毕竟,这三种技术恰好可以反映从原始的JDBC到现在流行的ORM的整个发展历程。
要说在整个项目中都是JDBC作为数据访问技术,不是说不行,但在目前看来,有了构建于JDBC之上的各种ORM产品可供选择的情况下,这并非最好 的方式, 毕竟,JDBC的API设计过于面向底层,无法有效的提高开发效率,即使有了Spring之类为JDBC提供的适当抽象层支持, 在整个项目开发中全部使用JDBC进行数据访问也不应该作为首选方案,当然,如果项目的数据访问需求很少,很简单,这也是可以接受的。
JDBC最大的优势在于其API贴近各关系数据库,可以最大限度的发挥各个关系数据库的特性,所以,对于存储过程的调用啦,基于关系数据库特定功能的数据访问操作啦,以及数据的批量更新之类操作, 这些东西就比较适合使用JDBC来完成。
具体开发过程中直接使用JDBC都会碰到的一个问题就是,需要使用String或者StringBuffer(Java5后推荐用 StringBuilder)来拼凑SQL语句。 在代码中拼凑SQL语句实际上是比较令人烦的,排版,特殊字符的转译等等,如果以后因为数据库Schema变更,你还需要回到代码中去重新编辑这些拼凑的 SQL语句, 可是,当你拼凑的SQL语句“身材魁梧”的话,你就得耗费比较长的时间来重新打量一下它,以免稍微不慎,就会一世英明毁于一个字符。
所以,即使是直接使用JDBC,我们也会需求将SQL语句抽取到外部单独文件的方式,毕竟,直接写SQL语句要比拼凑的过程舒服的多,而且,即使 schema变更, 我们也只是针对这个外部文件进行修改,而不用重新编辑并编译代码。 当然,即使要达到这样的目的,我们也不用亲自“下厨”,ibatis实际上就可以很大程度上满足我们的胃口。
ibatis在JDBC的基础上封装了薄薄的一层,提供了SQL语句的映射能力,同时添加了缓存支持,但ibatis不对持久化对象的状态进行管理。
在整个项目中使用ibatis作为数据访问技术,实际上是比较容易推行的,因为ibatis真的是很容易上手,即使你的团队之前没有任何 ibaits的使用经验,只要整个团队成员SQL没问题, 那么,读完简单几十页ibatis的Reference文档或者quick start文档之后,就可以很快的投入到开发当中去。ibatis的口号实际上就是“只要你会SQL,只要你知道怎么编辑XML,那么你就行!”,当然这是我给总结的。
因为ibatis与Jdbc似的,可以允许你最大程度上操作SQL层面,所以,对于一些遗留系统的数据库设计,使用ibatis也可以很容易的搞 定,却省却了直接使用jdbc的部分繁琐。 另外,对于一些需要对数据访问性能拥有更多控制权的应用程序,比如某些批处理,或者性能要求苛刻的应用,只要不是过分苛刻,那么使用ibatis作为类似 应用程序的数据访问技术是比较合适的选择。
当然,既然是面向SQL层面的映射,数据库schema的变动肯定会对sqlmap中的sql定义造成一定的冲击,牵扯到 CRUD(Create/Read/Update/Delete)操作的sql映射定义, 我们可能得修改多处,而且,当每一个数据库都牵扯基础的CRUD操作的时候,编写这些SQL也是比较繁琐的事情,虽然ibatis提供了自动生成CRUD 操作的映射工具Abator(http://ibatis.apache.org/abator.html),但不得不说的是, ibatis不适合对付类似CRUD这样的数据访问场景。
另外一个场景或许不是每个人都能遇到,但确实让我遇上了,为了能够可以控制数据访问的性能,并且考虑到ibatis的较低入门门槛,笔者在某个子项 目中采用了ibatis作为数据访问的手段, 应该说,最终是达到预期效果的。但是,当公司提出,针对同一产品,推出基于不同数据库版本的时候,使用ibatis作为数据访问手段则稍微造成一点儿麻 烦, 因为不同数据库的SQL差异,我们不得不为每一个版本重新调整SQL语句,虽然最终调整量不大,但对于这种情况来说,却令我怀念起Hibernate的 Dialect了。
即使像Hibernate这样功能完备的ORM方案,也不是说就适合任何场景的数据访问,Hibernate有其自己最适合的应用场景:
-
基本的CRUD数据访问操作;
-
批量查询,但却只对其中部分数据进行更新的场合;
-
查询对象需要进行缓存以提高整个系统的数据访问性能;
-
如果项目是从头开始,可以整个的把握数据库schema的设计,持久化对象与数据库设计之间有很好的契合关系;
对于一个之前没有任何Hibernate经验的团队来说,学习曲线高好像是自始至终都会提到的一个话题,但实际上,我觉得并没有像宣称的那样或者说 我们想像的那么高。 实际上,如果没有过多特殊的需求,让整个团队快速介入基于Hibernate的应用开发也不是过于困难。对于Hibernate正统的用法可能是各种映射 全部配置好, 但实际开发中,大部分团队使用的却是更加实际的用法,那就是由开发人员根据情况来维护各持久化类之间的映射关系,或许你会说这不够好,违背了 Hibernate的初衷, 可是如此多的开发团队这么使用,难道不能够说明一定的问题吗?
Hibernate针对不同数据库提供不同的Dialect支持,即使因为数据库迁移,你的数据访问代码的改动量也不是很大,而且,从某种程度上 说,Hibernate为各个Dialect提供的底层SQL是足够高效的,除非你的应用对SQL性能要求更加严格, Hibernate绝对是现在开发应用程序首选的数据访问技术。
从我的角度出发,如果从Jdbc,iBatis和Hibernate之间做出选择的话,通常情况下,项目伊始,当以Hibernate为首选方案, 毕竟,Hibernate在功能上的不断完善,将使它成为高效完备的数据访问技术,只有说确实存在特定的数据访问需求,Hibernate不能很好处理的 时候, 我们才需要寻求iBatis或者Jdbc的帮助。
相关推荐
Part I. Background 1. The Spring Data Project ...13. Creating Big Data Pipelines with Spring Batch and Spring Integration Part VI. Data Grids 14. GemFire: A Distributed Data Grid Bibliography Index
6. **DAO(Data Access Object)模式**:Spring推荐使用DAO模式来组织数据访问层,这样可以将业务逻辑和数据访问逻辑分离,提高代码的可测试性和可维护性。 7. **AOP(面向切面编程)**:Spring的AOP功能可用于实现...
《Just Spring Data Access中文版.pdf》带目录,老外写的书
标题“Spring Data Modern Data Access for Enterprise Java.pdf”明确指出了本书的核心内容是关于如何使用Spring Data框架来进行现代化的数据访问操作,尤其针对的是企业级Java应用开发。这表明本书不仅会介绍...
- **使用JdbcTemplate**:这是Spring中最常用的数据访问抽象层之一。它提供了一种灵活的方式来执行SQL语句,并返回结果集或受影响的行数。开发者可以通过传递SQL语句和参数来调用JdbcTemplate的方法,大大减少了代码...
总之,这个项目提供了一个完整的Spring MVC应用示例,其中整合了Spring Data JPA进行数据库操作,并使用JSON进行数据交换,对于学习Spring框架和JPA的初学者来说,是一个很好的实践平台。通过深入研究和运行这个项目...
1. **Spring Data JPA**:这是一个用于简化Java Persistence API(JPA)使用的模块,提供了基于注解的实体类,Repository接口,以及自动数据访问功能。通过Repository接口,你可以无需编写大量DAO代码就能实现基本的...
在Spring中,数据访问层(Data Access Layer)是应用程序的重要组成部分,它负责与数据库进行交互,执行CRUD操作(创建、读取、更新和删除)。Spring通过其IoC(控制反转)和AOP(面向切面编程)特性简化了这一过程...
This book is intended to give you a hands-on introduction to the Spring Data project, whose core mission is to enable Java developers to use state-of-the-art data processing and manipulation tools but...
SpringData是Spring框架的一个重要模块,它为Java开发者提供了对数据访问层的简化处理,特别是针对关系型数据库和NoSQL数据库。本节我们将探讨SpringData的概述以及如何通过一个简单的"HelloWorld"示例来入门。 ...
7. **异常处理**:Spring MVC和Spring Data都会捕获并处理可能出现的异常,如数据访问异常,然后转换为HTTP响应状态码和错误信息,方便前端展示。 总结来说,这个"Sample Spring MVC e-commerce application"展示了...
SpringData还支持异步操作,通过使用`@Async`注解,可以在不阻塞主线程的情况下执行数据访问任务,提升系统性能。 10. **SpringData Rest**: SpringData REST模块将Repository自动暴露为RESTful服务,方便前后端...
学习并熟练掌握Spring Data JPA对于任何使用Spring框架的开发者都是至关重要的,因为它可以帮助你构建更整洁、更易于维护的数据访问层。在实际项目中,应结合具体需求灵活运用这些特性,以达到最佳效果。
provides a rich ecosystem of projects to address modern application needs, like security, simplified access to relational and NoSQL datastores, batch processing, integration with social networking ...
在Spring框架中,数据访问对象(Data Access Object, DAO)是一种设计模式,它为应用程序提供了一种抽象层,用于处理底层的数据存储和检索操作。DAO模式的主要目标是将业务逻辑和数据访问逻辑分离,使代码更加模块化...
2. Spring Data框架的基本使用,包括其如何简化数据访问层的编程模型。 3. 使用Hibernate和Spring Data进行CRUD(创建、读取、更新、删除)操作的方法。 4. 对于持久化单元的配置和管理,这包括数据源配置、事务管理...
在现代Java开发中,通常还会引入Maven进行项目构建和依赖管理,以及Spring Data JPA来提高数据访问的便捷性。 首先,让我们详细了解一下SSH整合的关键点: 1. **Spring**:Spring框架提供了全面的面向切面编程...
SpringData 是一个强大的框架,主要用于简化 Spring 应用程序中数据访问层的开发。它提供了对多种持久化技术的统一接口,包括 JPA、MongoDB 和 Redis 等。在这个"springdata示例"中,我们将探讨如何使用 SpringData ...