第15章 Spring对各种ORM的集成

15.2 Spring对iBATIS的集成

iBATIS(http://ibatis.apache.org/)是我近一年多使用最多的ORM解决方案。如果将Hibernate比做自动步枪,而将JDBC比做手动步枪的话,那么iBATIS就得算是半自动步枪了。iBATIS并没有像Hibermate之类的ORM解决方案那样提供完备的ORM特性,包括对象查询语言、透明持久化等,但iBATIS却以其他的优势在这百花争艳的ORM武林中占有一席之地。相对于其他完备的ORM产品来说, iBATIS学习曲线很低,你以及你的团队只要精通SQL,那基本上就没有太多问题。

如果想以更加灵活的方式使用JDBC,而且不想引入过于复杂的ORM产品却想使用一定的ORM特性,那么,iBATIS应该是我们的最佳选择。不过,既然iBATIS也是一种依赖于数据资源的访问技术,那么,通常也避免不了像JDBC以及Hibernate在具体的使用过程中所遇到的资源管理、异常处理等各种方面的问题。

15.2.1 iBATIS实践之“前生”篇

在iBATIS中,通常是通过com.ibatis.sqlmap.client.SqlMapClient进行数据访问的。当然,在使用之前,我们需要先构建一个可用的实例,如下方代码所示。

 
    Reader reader = null;
    SqlMapClient sqlMap = null;
    try {
        String resource = "com/ibatis/example/sqlMap-config.xml";
    	reader = Resources.getResourceAsReader(resource);
    	sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
    } catch(IOException e) {
      e.printStackTrace(); // 不要这样做
    }
    // sqlMap现在处于可用状态
    
 

与其他ORM一样,iBATIS同样需要一个总的配置文件来获取具体的Sql映射文件。根据该配置文件所提供的配置信息,就可以使用sqlMapClientBuilder来构建一个可用的SqlMapClient实例了。而这之后,是通过Singleton方式还是工厂方式将这个实例公开给系统,就完全由你来决定了。

通常,我们可以通过以下三种方式来使用sqlMapClient进行数据访问。

基于sqlMapClient的自动提交事务型简单数据访问。 应该说,SqlMapClient对于资源的管理已经进行了足够的封装,使用SqlMapClient进行自动提交事务性质的数据访问,根本就不用考虑太多资源管理的问题,例如:

    Map parameters = new HashMap();
    parameters.put("parameterName", value);
    ...
    Object result=sqlMap.queryPorObject("System.getSystemAttribute", parameters);
    

基于sqlMapClient的非自动提交事务型数据访问。 虽然直接使用sqlMapClient进行数据访问可以暂时不考虑资源管理问题,但是在数据访问代码中加入事务控制代码以及异常处理代码之后,事情看起来就不像开始那么清爽了(见下方代码)。

 
    try {
    	sqlMap.startTransaction();
    	sqlMap.update("mappingStatement");
    	sqlMap.commitTransaction();
    } catch (SQLExceptione) {
    		e.printStackTrace(); // 不要这么做
    }
    finally
    {
    	try{
    		sqlMap.endTransaction();
      }catch(SQLExceptione) {
    		e.printStackTrace(); // 不要这么做
      }
    }
    

基于 sqlMapSession 的数据访问。 另外,也可以从 SqlMapClient 中获取 sqlMapSession,由自已来管理数据访问资源以及相关的事务控制和异常处理,如下方代码清单所示。

 
    
    SqlMapSession session = null;
    try {
      session = sqlMap.openSession();
      session.startTransaction();
      session.update("");
      session.commitTransaction();
    } catch(SqlException e) {
    		e.printStackTrace(); // 不要这么做
    }
    finally
    {
      if (session != null) {
        try{
          session.endTransaction();
        }catch(SQLExceptione) {
          e.printStackTrace(); // 不要这么做
        }
      }
    }

这种方式可能获取少许的性能优势,不过通常与第二种直接使用sqlMapClient进行数据访问没有太大分别,只不过把一些原来由SqlMapClient管理的任务拿来自己管理而已。

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

15.2.2 iBATIS实践之“今世”篇

因为Spring在集成iBATIS的时候,还要考虑将它的事务控制也纳入Spring统一的事务管理抽象层,所以使用了基于SqlMapSession的数据访问方式对iBATIS进行集成。这种方式更为灵活,可以将iBATIS内部可以直接指定的数据源以及事务管理器等“设备”转由外部提供,如使用Spring的IoC容器为其注入。

org.springframework.orm.ibatis.SqlMapClientTemplate是Spring为基于iBATIS的数据访问操作提供的模板方法类,我们可以通过sqlMapClientTemplate结合org.springframework.orm.ibatis.Sq1MapClientCallback回调接口完成所有基于iBATIS的数据访问操作。 SqlMapClientTemplate管理事务、异常处理、资源管理等方面的事情,而sqlMapClientCallback则使得开发人员专注于具体的数据访问逻辑。

1. SqlMapClientTemplate的实现

SqlMapClientTemplate中的execute(SqlMapClientCallback)方法是整个SqlMapClientTemplate实现的核心,如下定义:

 
    
    public Object execute(SqlMapClientCallback action) throws DataAccessException
    

所有其他的模板方法都是在该模板方法的基础上为了进一步提供便利而提供的。

execute(SqlMapClientCallback)模板方法以统一的方式,对基于iBATIS的数据访问操作进行了封装并于一处管理,可谓集资源管理、异常处理以及事务控制于一身 ,其定义的代码摘录如下方代码清单所示。

 
    
    public Object execute(SqlMapClientCallback action) throws DataAccessException {
      Assert.notNull(action, "Callback object must not be null");
      Assert.notNull(this.sqlMapClient, "No SqlMapClient specified");
      // We always needs to use a SqlMapSession, as we need to pass a Spring-managed
      // Connection (potentially transactional) in. This shouldn't be necessary if
      // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately
      // we still need it to make iBATIS batch execution work properly: If iBATIS
      // doesn't recognize an existing transaction, it automatically executes the
      // batch for every single statement...
      SqlMapSession session = this.sqlMapClient.openSession();
      if (logger.isDebugEnabled()) {
        logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation");
      }
      Connection ibatisCon = null;
    
      try {
        Connection springCon = null;
        DataSource dataSource = getDataSource();
        boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);
    
        // Obtain JDBC Connection to operate on...
        try {
          ibatisCon = session.getCurrentConnection();
          if (ibatisCon == null) {
            springCon = (transactionAware ? dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));
            session.setUserConnection(springCon);
            if (logger.isDebugEnabled()) {
              logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation");
            }
          } else {
            if (logger.isDebugEnabled()) {
              logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation");
            }
          }
        } catch (SQLException ex) {
          throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
        }
    
        // Execute given callback...
        try {
          return action.doInSqlMapClient(session);
        } catch (SQLException ex) {
          throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
        } finally {
          try {
            if (springCon != null) {
              if (transactionAware) {
                springCon.close();
              } else {
                DataSourceUtils.doReleaseConnection(springCon, dataSource);
              }
            }
          } catch (Throwable ex) {
            logger.debug("Could not close JDBC Connection", ex);
          }
        }
        // Processing finished - potentially session still to be closed.
      } finally {
        // Only close SqlMapSession if we know we've actually opened it
        // at the present level.
        if (ibatisCon == null) {
          session.close();
        }
      }
    }
    

该方法定义初看起来或许感觉繁杂,是因为Spring在集成iBATIS的时候,要考虑在整个框架内以统一的方式处理事务管理,才会出现这种初看起来不慎清晰的代码实现。

execute方法一开始通过给定的sqlMapClient获取相应的SqlMapSession:

 
    SqlMapSession session = this.sqlMapClient.openSession();
    

我们说过Spring在集成iBATIS的时候使用的是基于sqlMapSession的数据访问方式,所以,这行代码很好理解。

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

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

这实际上就是整个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了呢?

2. SqlMapClientTemplate的使用

SqlMapClientTemplate底层依赖于com.ibatis.sqlmap.client.SqlMapClient来提供基于iBATIS的数据访问支持,所以,要使用sqlMapClientTemplate,最少需要提供一个SqlMapClient。

SqlMapClientTemplate的构建

如果是通过编程的方式使用SqlMapClientTemplate,那么我们就可以像当初那样,直接使用iBATIS的方式,通过sqlMapClientBuilder构建相应的SqlMapClient实例,然后传给SqlMapClientTemplate,如下所示:

 
    String resource = "com/ibatis/example/sqlMap-config.xml";
    Reader reader = Resources.getResourceAsReader(resource);
    SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
    SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(sqlMap);
    // sqlMapC1ientTemplate准备就绪,可以使用
    

当然,只提供sqlMapClient实例的话,sqlMapClientTemplate将使用iBATIS配置文件内部指定的DataSource。我们也可以使用外部定义的DataSource,例如:

 
    // 1. 定义DataSource
    BasicDataSource dataSource = new BasicDataSource();
    ...
    // 2. 定义SqlMapClient
    SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
    ...
    // 3. 创建SqlMapClientTemplate
    SqlMapClientTemplate sqlMapClientTemplate = new SqlMapClientTemplate(dataSource, sqlMap);
    // sqlMapC1ientTemplate准备就绪,可以使用
    

实际上,除了直接通过编程的方式配置和使用sqlMapClientTemplate,更多时候我们是通过IoC容器来配置和使用SqlMapClientTemplate的。在Spring的IoC容器中,我们使用的org.springframework.orm.ibatis.sqlMapClientFactoryBean配置和获取相应的SqlMapClient,并注入给SqlMapClientTemplate使用。SqlMapClientFactoryBean也是一个FactoryBean实现,其getObject()方法返回的就是我们需要的SqlMapClient。

通过SqlMapClientFactoryBean配置和使用SqlMapClient以及SqlMapClientTemplate的情形如下方代码清单所示。

   
    <bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    	<property name="url">
    		<va1ue>${db.main.url}</value>
    	</property>
    	<property name="driverClassName">
    		<value>${db.main.driver}</value>
    	</property>
    	<property name="username">
    		<value>${db.main.user}</value>
    	</property>
    	<property name="password">
    		<value>${db.main.password}</value>
    	</property>
    </bean>
    
    <bean id="mainSqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
    	<property name="sqlMapClient">
    		<bean class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    			<property name="dataSource">
    				<ref bean="mainDataSource"/>
    			</property>
    			<property name="configLocation">
    				<value>file:conf/ibatis/ibatis-config.xml</value>
    			</property>
    		</bean>
    	</property>
    </bean>
    
 

如果容器中的SqlMapClientFactoryBean仅限于一个SqlMapClientTemplate使用的话,可以像上方代码清单所示的配置那样,将sqlMapClientFactoryBean配置为内部bean的形式。当然,这不是必须的。

使用SqlMapClientCallback进行数据访问

SqlMapClientCallback在整个Spring对iBATIS的集成中所处的地位仅次于SqlMapClientTemplate。通过SqlMapClientTemplate的execute(SqlMapClientCallback)模板方法结合SqlMapClientCallback,我们可以完成任何基于iBATIS的数据访问操作。

SqlMapClientCallback的定义很简单(通常Callback接口都很简单),通过doInSqlMapClient()方法公开了com.ibatis.sqlmap.client.SqlMapExecutor用于具体的数据访问操作,如下所示:

使用SqlMapClientCallback进行数据的批量更新操作代码示例:

 
    protected void batchInsert(final List beans) {
    	sqlMapClientTemplate.execute(new SqlMapClientCallback() {
    		public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
    			executor.startBatch();
    			Interator iter = beans.iterator();
    			while(iter.hasNext()) {
    				Bean bean = (Bean) iter.next();
    				executor.insert("insert_name", bean);
    			} 
    			executor.executeBatch();
    			return null;
    		}
    	});
    }
    
 

至于其他基于iBATIS的基本的数据访问操作,也可以用类似的方式为sqlMapClientTemplate提供相应的SqlMapClientCallback实现。不过,通常情况下,我们不需要直接这么做,因为sqlMapClientTemplate为我们考虑得更多。

基于SqlMapClientTemplate的基本数据访问操作

如果要使用SqlMapClientTemplate进行基于iBATIS的一些基本的数据访问操作,那么不需要每次都提供一个SqlMapClientCallback实现,然后通过execute(SqlMapClientCallback)来执行这些操作。为了便利起见,SqlMapClientTemplate为这些基本数据操作提供了足以满足需要的多组模板方法,我们所要做的,只是根据需要来选用这些便利的模板方法而已。

所有的SqlMapClientTemplate中定义的数据访问操作方法都在org.springframework.orm.ibatis.sqlMapClientoperations接口中定义,也包括这些便利的模板方法。实际上,在开发过程中,借助于IDE的支持,只要稍微参照一下Javadoc就能娴熟地使用这些模板方法。下方代码清单给出了其中部分模板方法的使用代码示例。

   
    SqlMapClientTemplate sqlMapClientTemplate = ...;
    Object parameter = ...;
    // 1.插入数据
    sqlMapClientTemplate.insert("insertStatementName", parameter);
    // 2.更新
    int rowAffected = sqlMapClientTemplate.update("updateStatementName");
    // 3.删除
    int rowAffected = sqlMapClientTemplate.delete("deleteStatementName", parameter);
    // 4.查询
    Object result = sqlMapClientTemplate.queryForObject("selectStatementName");
    List resultList = sqlMapClientTemplate.queryForList("selectStatementName");
    Map resultMap = sqlMapClientTemplate.queryForMap("selectStatementName");
    sqlMapClientTemplate.queryWithRowHandler("hugeSelect", newRowHandler() {
    	public void handleRow(Object valueObject} {
    		ResultBean bean = (ResultBean)valueObject;
    		process(bean);
    }});
    
 

更多信息可以参照SqlMapClientTemplate的Javadoc和iBATIS的参考文档以及相关书籍。

3.SqlMapClientDaoSupport

与集成其他数据访问方式同等待遇,如果要使用BATIS实现相应的DAO的话,Spring为我们提供了org.springframework.orm.ibatis.support.SqlMapClientDaoSupport作为整个DAO层次体系的基类。

所有开发人员在使用iBATIS进行DAO开发的时候,直接继承SqlMapClientDaoSupport类即可获得SqlMapClientTemplate的数据访问支持,如下方代码清单所示。

   
    public class FooSqlMapClientDao extends SqlMapClientDaoSupport implements IFooDao {
    	public void update(YourBean bean) {
    		getSqlMapClientTemplate.update("updateStatementName", bean);
      }
    	public Object get(String pk) {
    	return getSqlMapC1ientTemplate().queryForObject("selectStmtName", pk);
      }
      ...
    }
    

当然,如果愿意,直接为相应的DAO注入sqlMapClientTemplate来使用也没有人会反对,尤其是在系统中旧有的DAO基类已经存在的情况下。