首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 数据库 > Access >

使用Spring开展数据访问(Data Access With Spring)

2013-09-25 
使用Spring进行数据访问(Data Access With Spring)1.1.?统一的数据访问异常层次体系(Consistent Exception

使用Spring进行数据访问(Data Access With Spring)
1.1.?统一的数据访问异常层次体系(Consistent Exception Hierarchy In Spring)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 stubreturn 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 stubreturn null;}public void updateCustomerStatus(Customer customer) {// TODO Auto-generated method stub}}
即使数据访问接口实现类随着需求发生变化,可是客户端代码(这里的CustomerService)却可以完全忽视这种变化, 唯一需要变动的地方可能只是Factory对象的几行代码甚至只是IoC容器配置文件中简单的class类型替换而已,而客户端代码无需任何变动。 所以,DAO模式对屏蔽不同数据访问机制的差异性起到举足轻重的作用。

?

但是,图画真的像我描述的那么美好吗?!

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);//...}
可是,这样就解决问题了吗?Customer findCustomerByPK(String customerId) throws SQLException,NamingException;糟糕不是吗?我们又把统一的访问接口给改了,相应的客户端代码又要捕捉NamingException做相应的处理,如果随着不同数据访问对象实现的增多,以及考虑数据访问对象中其他数据访问方法,这种糟糕的情况不得继续下去吗?

?

也就是说,因为数据访问机制的不同,我们的数据访问接口的定义现在变成了空中楼阁,我们无法最终定义并确定下这个接口,不是吗?

?

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”类型的面向数据访问领域的异常层次体系。


OptimisticLockingFailureException对应数据库更新时候出现乐观锁冲突的情 况;PessimisticLockingFailureException自然对应的是悲观锁冲突啦,不 过,PessimisticLockingFailureException下面还可以为细分为CannotAcquireLockException和 DeadlockLoserDataAccessException等特定的异常类型。

?

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的javadoc获取这些Exception类型和他们的子类型的更多信息。

?

不管怎么样,到此为止,我想您已经对于Spring为什么要提供这么一套标准的异常层次体系以及如何发挥这套异常层次体系的最大作用了然于心了吧?! 那么让我们继续前进,开始了解Spring提供的“JDBC API的最佳实践”!

1.2.?JDBC API的最佳实践(JDBC made easy with spring)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实现的时候,我可以保证你能够发现更多的“惊喜”:

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 {@Overrideprotected void putIntoGear() {// 挂前进档位}}// 手动档汽车public class VehicleMT extends Vehicle {@Overrideprotected void putIntoGear() {// 踩离合器// 挂前进挡位}}
这样一来,就不需要每个子类中都声明并实现共有的逻辑,而只需要实现特有的逻辑就行了。

?

ResultSet rs = stmt.executeQuery(sql);while(rs.next()){processResultRow(rs);}

?

stmt.close(); stmt = null;

关闭相应的Statement或者PreparedStatement;

catch(SQLException e){...}

处理相应的数据库访问异常;

finally{con.close();}

关闭数据库连接以避免连接泄漏导致系统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底层相关的细节却不用过多的考虑,进而也不会出现令人恼火的数据库连接没释放的问题了。


// --- 摘自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实现中的更多细节,咱们接着看...

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的方式就多了这些东西。

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实现类:

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?

?

public interface SQLExceptionTranslator {DataAccessException translate(String task, String sql, SQLException ex);}该接口有两个主要实现类,分别为 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator和 org.springframework.jdbc.support.SQLStateSQLExceptionTranslator:
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进行异常转译的整个流程中,我们可以在两个点插入自定义的异常转译逻辑:

public class ToySqlExceptionTranslator extendsSQLErrorCodeSQLExceptionTranslator {@Overrideprotected 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 SQLErrorCodeSQLExceptionTranslatorSQLErrorCodeSQLExceptionTranslator sqlExTranslator = new ToySqlExceptionTranslator();sqlExTranslator.setDataSource(dataSource);jdbcTemplate.setExceptionTranslator(sqlExTranslator);// do data access with jdbcTemplate ...
至此,通过扩展SQLErrorCodeSQLExceptionTranslator以达到自定义异常转译的目的已经达到,不过,与下面的方式比起来, 具体实践上面则要稍逊一筹啦。

?

<?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" name="code"><bean id="MyDB" name="code"><bean id="DB2" name="code">BasicDataSource dataSource = new BasicDataSource();dataSource.setDriverClassName("com.mysql.jdbc.Driver");dataSource.setUrl("jdbc:mysql://localhost/mysql?useUnicode=true&amp;characterEncoding=utf8&amp;failOverReadOnly=false&amp;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" 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" name="code">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异常。

?

如果查询的结果将返回多行,而你又不在乎他们是否拥有较强的类型约束,那么,以下模板方法可以帮助你:

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; }

?

为了说明这三种Callback接口的使用和相互之间的区别,我们暂且设定如下场景: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);}});
如果你没有发现最大的差异在哪里,那么容我细表: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;}}

?

总的来说,内部工作归根结底是由ResultSetExtractor做了,RowCallbackHandler和RowMapper只是为了帮助我们 简化使用上的操作而已。 所以,实际使用中,RowCallbackHandler和RowMapper才是我们最常用的选择。

?

对于使用JdbcTemplate进行查询,基本就这些内容了,当然,如果你非要使用基于StatementCallback之类更底层的 execute方法的话,那就是你个人说了算啦。 不过,要想知道JdbcTemplate中有关查询相关模板方法的更多信息,在实际使用中参考JdbcTemplate的javadoc就可以,当然,有 IDE就更便捷了。

// insert jdbcTemplate.update("insert into customer(customerName,age,...) values("darren","28",...)");// updateint 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 (...)");// orjdbcTemplate.execute("drop table customer");
至于其他重载的execute()方法,相对来说过于贴近JDBC API了,通常情况下,我们没有必要使用,某些时候为了集成遗留系统中某些基于Jdbc的数据访问代码倒是有可能需要求助于这些execute方法。

?

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有两个方法需要我们实现: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: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 paramterscs.setString(1, "tableName");cs.setInt(3, 1);cs.registerOutParameter(2, Types.VARCHAR);cs.registerOutParameter(3, Types.INTEGER);// execute Callcs.execute();// extract result and returnMap 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 returnMap 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中另一种存储过程调用方式。

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的相关实现类可以分为两类:
CREATE TABLE fx_news ( news_id bigint(20) NOT NULL, new_titlevarchar(25) NOT NULL, new_body text NOT NULL, PRIMARY KEY(news_id))news_id为该表的主键,要使用MySQLMaxValueIncrementer(或者HsqlMaxValueIncrementer)每次插入数据的时候为该字段生成主键值, 我们还需要定义对应fx_news表的主键表,用来计算并保持当前主键值:
CREATE TABLE fx_news_key (     valuebigint(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" 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" ref="dataSource"/><property name="incrementerName" value="fx_news_key"/><property name="columnName" value="value"/></bean><bean id="jdbcTemplate" ref="dataSource"/></bean><bean id="djNewsPersister" ref="incrementer"/><property name="jdbcTemplate" ref="jdbcTemplate"/></bean>
DowJonesNewsPersister每次向数据新追加新闻数据的时候,就可以使用为其注入的incrementer递增主键了。

?

使用IoC容器来管理相应的DataFieldMaxValueIncrementer,可以将系统中所有的 DataFieldMaxValueIncrementer实例集中到一个配置模块中,以便于管理和使用。 不过,使用容器管理的DataFieldMaxValueIncrementer可能需要注意一个问题,那就是,容器中的各个 DataFieldMaxValueIncrementer虽然在系统中可以共享, 但即使是在系统不使用的情况下,相应的实例也不会释放,除非系统推出,所以,对于系统资源紧张的应用来说,在合适的时机根据需要实例化相应的 DataFieldMaxValueIncrementer来使用也不失为合适的方式。

CREATE SEQUENCE fx_news_seqNCREMENT 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_seqNCREMENT  BY   1 START  WITH   1 NOMAXVALUE      NOCYCLE        CACHE 5;
当然啦,如果应用程序构建于Spring的IoC容器之上,从“基于主键表的DataFieldMaxValueIncrementer实现类”变为“基于Sequence的DataFieldMaxValueIncrementer实现类”仅仅是简单的配置更改而已:
<bean id="dataSource" destroy-method="close">...</bean><bean id="incrementer" ref="dataSource"/><property name="incrementerName" value="fx_news_seq"/></bean>...<bean id="djNewsPersister" ref="incrementer"/><property name="jdbcTemplate" ref="jdbcTemplate"/></bean>
其他的bean定义和使用DataFieldMaxValueIncrementer的类逻辑基本不用动。

?

总之,如果需要递增的主键生成策略,根据应用程序使用的数据库选用相应的DataFieldMaxValueIncrementer实现类即可,如 果Spring框架没有提供当前应用程序所用数据库的DataFieldMaxValueIncrementer实现类,那不妨在 AbstractDataFieldMaxValueIncrementer或者 AbstractSequenceMaxValueIncrementer的基础上扩展一个啦!

CREATE TABLE images ( id int(11) NOT NULL, filename varchar(200) NOT NULL, entity blob NOT NULL, descriptiontext 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的继承关系如下图所示:


public abstract class AbstractLobHandler implements LobHandler {public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException {return getBlobAsBytes(rs, rs.findColumn(columnName));}...}所以,我们应该更多的关注OracleLobHandler和DefaultLobHandler这两个具体实现类:OracleLobHandler lobHandler = new OracleLobHandler();lobHandler.setNativeJdbcExtractor(new CommonsDbcpNativeJdbcExtractor());// lobHandler ready to use

?

除了Oracle之外的大多数数据库可以使用DefaultLobHandler作为他们的LobHandler实现用来操作LOB数据。 DefaultLobHandler主要通过标准的JDBC API来创建和访问LOB数据。

现在,对“images”表的数据进行创建和访问就不会因为因数据库的变动而需要做相应的调整了,要调整的仅仅是选择使用哪一个LobHandler实现类而已, 而通常这仅需要调整一行配置即可。

?

使用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){@Overrideprotected 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 fileFile 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(){@Overrideprotected 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容器管理整个应用的配置和运行。

前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=:descriptionSqlParameterSource 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对象定义的属性名称一致。

DataSource dataSource = ...;NamedParameterJdbcTemplate npJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);// or you have got a JdbcTemplateJdbcTemplate 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基础上多添加一层“解析”网。

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的基础上套了另一层网而已。

// 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"        destroy-method="close">    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>    <property name="url" value="jdbc:mysql://localhost/mysql?useUnicode=true&amp;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" 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家族,DBCP和C3P0将是我们最常用的DataSource实现,无论是开发,测试还是生产环境,最重要的是,你可以在Spring容器中很容易的配置和使用它们。

?

<bean id="dataSource" name="code"><?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的命名空间加入配置文件就行了。

?

protected abstract Object determineCurrentLookupKey();每次getConnection()请求将被导向那个DataSource,将有这个方法的逻辑来决定。

?

AbstractRoutingDataSource的整个场景类似于下图所示:


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目 标对象上获取。

你可以根据情况选用这些现成的实现类,或者决定根据需求实现自己的。

?

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使用方式”!

?



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();}@Overrideprotected 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 dataSource = ...; // DBCP or C3P0 or whateverFXNewsQueryWithParameters 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方法最后两个参数:

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方法最后一个参数取得:
@Overrideprotected 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传入的信息并不限于简单的字符串前缀啦,这要根据你的应用场景来决定。

?

可以看得出,mapRow方法的最后两个参数在日常开发中并非经常用到,鉴于此,Spring框架给出了MappingSqlQueryWithParameters的另一个子类,即MappingSqlQuery。

?

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();}@Overrideprotected 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将是我们最经常使用的查询操作对象实现了。

DataSource dataSource = ...; // DBCP or C3P0 or whateverSqlFunction 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()方法!

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();}@Overrideprotected 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 whateverString            sql = "select * from fx_news";CapitalTitleUpdateableSqlQuery updatableQuery = new CapitalTitleUpdateableSqlQuery(dataSource,sql);updatableQuery.execute();
可能你也注意到了,updateRow方法最后一个参数为“Map context”,它的作用 跟我们在讲解MappingSqlQueryWithParameters的mapRow方法所提到的最后一个参数是一样的。 在我们使用可更新结果集对查询结果进行更新的过程中,如果需要某些外部信息的话,我们可以在调用UpdatableSqlQuery数据访问操作方法的时 候通过Map传入,而在updateRow方法中通过“Map context”参数取得, 使用方式可以参照MappingSqlQueryWithParameters部分针对该参数的使用实例。

?

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();}@Overrideprotected 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数据写入了文件系统。

?

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进行数据更新,我们同样可以通过继承的方式来使用它,这样我们可以得到的好处有:

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则是比较合适的。

?

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过于繁琐的话,你也可以通过扩展它对其进行封装。

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有几点需要说明:declareParameter(new SqlParameter(Types.BLOB));其他的LOB类型处理需要根据具体应用场景进行调整。

?

在更新操作对象中,要为LOB型占位符参数提供参数值,需要使用 org.springframework.jdbc.core.support.SqlLobValue, SqlLobValue将使用给定的LobHandler对具体的Lob数据进行处理。SqlLobValue的javadoc中同样有结合 SqlLobValue和JdbcTemplae进行Lob数据更新的实例。

有了ImageLobDataUpdate,剩下我们做的就是提供要替换的图片文件,指定要对数据库中哪个文件名对应的数据进行更新替换罢了:
ImageLobDataUpdate update = new ImageLobDataUpdate(dataSource);update.replaceImageData("snow_image.jpg", new File("43.jpg"));
就是这么简单!(当然,不要忘记异常的处理哦!)

?

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;}}
关于该存储过程操作对象,部分细节我们有必要关注一下:private static final String PROCEDURE_NAME = "CountTable";如果有多个存储过程的参数顺序相同,结果处理也一样的话,你也可以将存储过程的名称声明为变量,这完全要取决于具体的应用场景。

?

在构造方法中,我们将“setFunction(true);”注释掉了,因为我们调用的CountTable不是一个Function, 如果你要调用的存储过程类型为Function的话,你需要通过该方法将“function”的值设置为true,以告知StoredProcedure在处理调用的时候要区别对待。

在complie之前通过declareParameter声明参数,这几乎是雷打不动的惯例,不过,在StoredProcedure中使用declareParameter的时候却要有所注意了:

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方法声明的参数名称一一对应。

?

通过扩展StoredProcedure,我们不但封装了参数的声明和结果的提取,我们还为调用方提供了强类型的调用方法, 现在,调用方可以通过doCountTable方法的强类型参数声明传入参数值,并取得强类型的CountTableResult对象作为结果,而不是泛 泛的一个Map。

?

对于存储过程的调用者来说,它的代码现在可以简洁到两行代码:

// 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对于我们来说没有太大用处罢了。

?

1.3.?Spring对各种ORM的集成(ORM Integration In Spring)

public class HibernateUtil {private static Log log = LogFactory.getLog(HibernateUtil.class);private static final SessionFactory sessionFactory;static {try {// Create the SessionFactorysessionFactory = 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 yetif (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的使用中解放出来,使其能够更加高效的工作,从而促进整个软件产品的质量提高。

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也已经为我们做了,基本上也不需要我们自己劳神。

public static DataAccessException convertHibernateAccessException(HibernateException ex)HibernateTemplate内部在实现HibernateException的异常转译的时候最终也是委派该方法进行的。 至于模板方法中需要处理的SQLException,HibernateTemplate则将这部分的转译工作转交给了 SQLExceptionTranslator,我想,在经历了JdbcTemplate部分的内容之后,我们对其并不陌生。

?

如果我们使用HibernateTemplate进行基于Hibernate的数据访问的话,我们可以无需关心具体的异常转译,因为 HibernateTemplate内部可以很好的处理这一点。 不过,即使你不能够使用HibernateTemplate,那也没关系,在你的基于Hibernate数据访问代码中,只要你愿意,依然可以借助 SessionFactoryUtils的convertHibernateAccessException方法进行异常的转译。

SessionFactory sessionFactory = ...;HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory);Object po = hibernateTemplate.get(FXNewsBean.class,new Integer(1));if(po == null)// not foundelse// process information with FXNewsBean要让这段代码运行,前提当然是Hibernate的映射文件以及PO等准备就绪 ,有关如何配置Hibernate进行数据访问请参照Hibernate的参考文档以及相关书籍。

?

至于其他几组查询方法,留待你去验证和使用吧!

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);

?

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,那么,这些基本的方式就先放在一边不谈。

?

<bean id="dataSource" 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" 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" 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文档。

<bean id="sessionFactory" 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>

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统一异常体系的转译支持。

?

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进行数据访问:

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管理的任务拿来自己管理而已。

?

实际上,你可以看出,除了基于SqlMapClient的自动提交事务性的数据访问方式之外, 其他两种方式在引入事务管理和异常处理甚至资源管理等方面的代码之后,整个的数据访问代码就变得有些难以管理了,如果让每一个开发人员在实现dao的时候 或者在service层管理事务的时候都使用如上几乎相同的代码, 而不是采用某种方式统一管理一下,那么,整个系统的稳定性和产品质量就可能存在问题了。

?

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的时候要考虑在整个框架内以统一的方式处理事务管理才会出现这初看起来不慎清晰的代码实现。SqlMapSession session = this.sqlMapClient.openSession();我说过spring在集成ibatis的时候使用的是基于SqlMapSession的数据访问方式,所以,这行代码很好理解;

?

Connection ibatisCon = null;这行代码一直到// Execute given callback...这行注释之前, 你都可以看作是spring在处理对事务管理的集成,spring会根据当前的事务设置来决定通过何种方式从指定的dataSource中获取相应的Connection供SqlMapSession使用;

之后的代码就比较容易理解了,模板方法会调用SqlMapClientCallback的回调方法来进行数据访问,如果期间出现 SQLException,则通过提供的SQLExceptionTranslator进行异常转译, 最后,合适的关闭使用的数据库连接和SqlMapSession完事。

这实际上就是整个SqlMapClientTemplate的奥秘所在。

?

为了避免开发人员在一些通用的数据访问操作上提供几乎相同的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更加了解了那?

?

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 definitionBasicDataSource dataSource = new BasicDataSource();...// 2. SqlMapClient definition...SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);// 3. construct the SqlMapClientTemplateSqlMapClientTemplate 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" 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" name="code">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 sqlMapClientTemplate = ...;Object parameter = ...;// 1. insert sqlMapClientTemplate.insert("insertStatementName",parameter);// 2. update int rowAffected = sqlMapClientTemplate.update("updateStatementName"); // 3. deleteint rowAffected = sqlMapClientTemplate.delete("deleteStatementName",parameter); // 4. queryObject 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的参考文档以及相关书籍。

?

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基类已经存在的情况下。

?

<bean id="mainDataSource" 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" 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" value="classpath:jdo-config.properties"/></bean>
如果你愿意,可以将该实例注入容器中任何依赖于PersistenceManagerFactory的bean定义。

?

创建完成的PersistenceManagerFactory可以注入spring为JDO提供的模板方法类org.springframework.orm.jdo.JdoTemplate,之后,所有的数据访问操作直接经由JdoTemplate进行。

?

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本身就是实现了该接口以便为开发人员提供统一的数据访问接口。

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);...

?

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实现体系, 但是,对于后者来说,往往是比较危险的举动,毕竟,一个团队内部的开发需要保持一致性,重新设立一套实现体系在技术层次上当然是比较合适的,但从管理和维 护的角度,则确实有值得商榷的地方。

<bean id="mainDataSource" 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" 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 DataAccessException
org.springframework.orm.toplink.TopLinkCallback是该核心模板方法所使用的回调接口,开发人员可以通过该回调接口提供具体的数据访问逻辑实现:
public interface TopLinkCallback {Object doInTopLink(Session session) throws TopLinkException;}
TopLinkCallback为开发人员暴露了Session资源以供数据访问的使用,而对于该资源的获取和释放则由TopLinkTemplate统一管理。

?

按照惯例,TopLinkTemplate在提供核心模板方法以满足一般数据访问需求的情况下,还会在该核心模板方法的基础上提供更多便于基本数据 访问操作的模板方法, 所有这些模板方法全部由org.springframework.orm.toplink.TopLinkOperations接口来定 义,TopLinkTemplate实现了该接口,所以提供了所有这些模板方法的实现以供我们使用。

public static DataAccessException convertTopLinkAccessException(TopLinkException ex)当然,无论如何,通过TopLinkTemplate进行基于toplink的数据访问通常才是比较合适的方式。

?

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基类的存在价值。

?

<bean id="entityManagerFactory" value="yourPersistenceUnitName"/></bean>这种方式应用场景有限,对于更多定制需求,可以考虑使用LocalContainerEntityManagerFactoryBean。

?

使用LocalContainerEntityManagerFactoryBean .? org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean是比较常用的 配置和获取EntityManagerFactory的方式,通过它, 我们可以指定自定义的配置文件位置,独立的dataSource定义甚至spring提供的定制字节码转换的loadTimeWeaver实现:

<bean id="mainDataSource" 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" ref="mainDataSource"/><property name="loadTimeWeaver"><bean value="META-INF/main-persistence.xml"/></bean>

?

通过JNDI获取EntityManagerFactory.? 我们也可以使用绑定到JNDI的EntityManagerFactory,对于基于Spring的ioc容器的应用来说,仅仅也就是配置文件的少许改动:

<bean id="dataSource" name="code"><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。

public static DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex)如果需要,我们同样可以直接使用该工具类提供的这个方法来完成到spring统一异常体系的转译。

?

1.4.?Spring数据访问扩展篇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 ftpUtility = ...;if(ftpUtility.login(..)){ftpUtility.doTransfer(..);}ftpUtiligy.logout();而且,你把资源的管理下放给了每一处调用Phase1FtpUtility进行ftp操作的调用代码,就跟数据库连接一样,你又能如何保证相应的资源在每 一处都获得释放那? 我想,就现有的API封装方式,我们只能加强开发人员的约束力来达到正确使用API的目的了。

?

Phase1FtpUtility的doTransfer(..)方法通常用来实现具体的ftp操作逻辑,那么,现在的 Phase1FtpUtility只能提供固定的ftp操作逻辑,如果其他调用方需要不同的ftp操作, 那么,或许得子类化Phase1FtpUtility并覆写(Override)doTransfer(..)方法了,不过,这样好像偏离了我们要将 Phase1FtpUtility作为单一工具类进行使用的初衷。

鉴于这些限制,我们需要另一种FTPClient API的封装方式,而你也看出来了,实际上,就如Phase1FtpUtility所展示的那样,所有的使用FTPClient进行ftp操作的步骤几乎 是一样的, 唯一的不同就是每次进行ftp操作细节,那么,在经历了JdbcTemplate,HibernateTemplate以及 SqlMapClientTemplate之类Template的熏陶之后,我们自然而然就应该想到, 我们同样可以对FTPClient API进行同样的处理,FTPClientTemplate就是我们最终的需要:
public class FTPClientTemplate {// private static final Log logger = LogFactory.getLog(FTPClientTemplate.class);// private FTPClientConfig ftpClientConfig;// optional //private String server;// requiredprivate String username; // requiredprivate String password; // requiredprivate int    port=21;// optionalpublic 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 confirmedint reply = ftp.getReplyCode();if(!FTPReply.isPositiveCompletion(reply)) {        throw new IOException("failed to connect to the FTP Server:"+server);}// loginboolean 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 methodcallback.processFTPRequest(ftp);// logoutftp.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来实现该接口。

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(); } }}又是获取资源,操作,释放资源,处理异常等等,而且这样的代码无法用于生产环境也是肯定的了,那该怎么处理我想你已经心里有数了吧?!余下的部分,还是留给你来实现它吧!

?

Note

当然,这并不是说当初选择ibatis是错误的决定,只不过,确实因为需求的变更,在迁移上较之其他方案要稍微费力一些而已。

?

1.4.2.3.?Hibernate选择漫谈

即使像Hibernate这样功能完备的ORM方案,也不是说就适合任何场景的数据访问,Hibernate有其自己最适合的应用场景:

基本的CRUD数据访问操作;

批量查询,但却只对其中部分数据进行更新的场合;

查询对象需要进行缓存以提高整个系统的数据访问性能;

如果项目是从头开始,可以整个的把握数据库schema的设计,持久化对象与数据库设计之间有很好的契合关系;

当然,具体的使用Hibernate的场景界限正随着Hibernate功能的增强而逐渐淡化,即使像批量更新,存储过程调用等之前不适合使用Hibernate的地方, 也会因为hibernate在更高版本中加入更多特性而变得可以直接通过Hibernate进行。

?

对于一个之前没有任何Hibernate经验的团队来说,学习曲线高好像是自始至终都会提到的一个话题,但实际上,我觉得并没有像宣称的那样或者说 我们想像的那么高。 实际上,如果没有过多特殊的需求,让整个团队快速介入基于Hibernate的应用开发也不是过于困难。对于Hibernate正统的用法可能是各种映射 全部配置好, 但实际开发中,大部分团队使用的却是更加实际的用法,那就是由开发人员根据情况来维护各持久化类之间的映射关系,或许你会说这不够好,违背了 Hibernate的初衷, 可是如此多的开发团队这么使用,难道不能够说明一定的问题吗?

Hibernate针对不同数据库提供不同的Dialect支持,即使因为数据库迁移,你的数据访问代码的改动量也不是很大,而且,从某种程度上 说,Hibernate为各个Dialect提供的底层SQL是足够高效的,除非你的应用对SQL性能要求更加严格, Hibernate绝对是现在开发应用程序首选的数据访问技术。

从我的角度出发,如果从Jdbc,iBatis和Hibernate之间做出选择的话,通常情况下,项目伊始,当以Hibernate为首选方案, 毕竟,Hibernate在功能上的不断完善,将使它成为高效完备的数据访问技术,只有说确实存在特定的数据访问需求,Hibernate不能很好处理的 时候, 我们才需要寻求iBatis或者Jdbc的帮助。?

热点排行