对ibatis分页功能的改进

今天无意间看到了一篇关于这方面的文章,觉得是网上改进ibatis分页方面比较好的文章,这里转摘一下,希望能让更多的人用的到,也希望别人能把更好的解决方案贡献出来!

<script language="javascript" src="使ibatis支持hibernate式的物理分页 - duanaiguo的专栏 - CSDNBlog.files/urltag.htm" type="text/javascript"></script>
<script type="text/javascript">function StorePage(){d=document;t=d.selection?(d.selection.type!='None'?d.selection.createRange().text:''):(d.getSelection?d.getSelection():'');void(keyit=window.open('http://www.365key.com/storeit.aspx?t='+escape(d.title)+'&u='+escape(d.location.href)+'&c='+escape(t),'keyit','scrollbars=no,width=475,height=575,left=75,top=20,status=no,resizable=yes'));keyit.focus();}</script>

一直以来ibatis的分页都是通过滚动ResultSet实现的,应该算是逻辑分页吧。逻辑分页虽然能很干净地独立于特定数据库,但效率在多数情况下不及特定数据库支持的物理分页,而hibernate的分页则是直接组装sql,充分利用了特定数据库的分页机制,效率相对较高。本文讲述的就是如何在不重新编译ibatis源码的前提下,为ibatis引入hibernate式的物理分页机制。

基本思路就是找到ibatis执行sql的地方,截获sql并重新组装sql。通过分析ibatis源码知道,最终负责执行sql的类是com.ibatis.sqlmap.engine.execution.SqlExecutor,此类没有实现任何接口,这多少有点遗憾,因为接口是相对稳定契约,非大的版本更新,接口一般是不会变的,而类就相对易变一些,所以这里的代码只能保证对当前版本(2.1.7)的ibatis有效。下面是SqlExecutor执行查询的方法:

   /**
   * Long form of the method to execute a query
   *
   * 
@param  request - the request scope
   * 
@param  conn - the database connection
   * 
@param  sql - the SQL statement to execute
   * 
@param  parameters - the parameters for the statement
   * 
@param  skipResults - the number of results to skip
   * 
@param  maxResults - the maximum number of results to return
   * 
@param  callback - the row handler for the query
   *
   * 
@throws  SQLException - if the query fails
   
*/
  
public   void  executeQuery(RequestScope request, Connection conn, String sql, Object[] parameters,
                           
int  skipResults,  int  maxResults, RowHandlerCallback callback)
      
throws  SQLException {
    ErrorContext errorContext 
=  request.getErrorContext();
    errorContext.setActivity(
" executing query " );
    errorContext.setObjectId(sql);

    PreparedStatement ps 
=   null ;
    ResultSet rs 
=   null ;

    
try  {
      errorContext.setMoreInfo(
" Check the SQL Statement (preparation failed). " );

      Integer rsType 
=  request.getStatement().getResultSetType();
      
if  (rsType  !=   null ) {
        ps 
=  conn.prepareStatement(sql, rsType.intValue(), ResultSet.CONCUR_READ_ONLY);
      } 
else  {
        ps 
=  conn.prepareStatement(sql);
      }

      Integer fetchSize 
=  request.getStatement().getFetchSize();
      
if  (fetchSize  !=   null ) {
        ps.setFetchSize(fetchSize.intValue());
      }

      errorContext.setMoreInfo(
" Check the parameters (set parameters failed). " );
      request.getParameterMap().setParameters(request, ps, parameters);

      errorContext.setMoreInfo(
" Check the statement (query failed). " );

      ps.execute();
      rs 
=  getFirstResultSet(ps);

      
if  (rs  !=   null ) {
        errorContext.setMoreInfo(
" Check the results (failed to retrieve results). " );
        handleResults(request, rs, skipResults, maxResults, callback);
      }

      
//  clear out remaining results
       while  (ps.getMoreResults());

    } 
finally  {
      
try  {
        closeResultSet(rs);
      } 
finally  {
        closeStatement(ps);
      }
    }

  }

 

其中handleResults(request, rs, skipResults, maxResults, callback)一句用于处理分页,其实此时查询已经执行完毕,可以不必关心handleResults方法,但为清楚起见,下面来看看handleResults的实现:

private   void  handleResults(RequestScope request, ResultSet rs,  int  skipResults,  int  maxResults, RowHandlerCallback callback)  throws  SQLException {
    
try  {
      request.setResultSet(rs);
      ResultMap resultMap 
=  request.getResultMap();
      
if  (resultMap  !=   null ) {
        
//  Skip Results
         if  (rs.getType()  !=  ResultSet.TYPE_FORWARD_ONLY) {
          
if  (skipResults  >   0 ) {
            rs.absolute(skipResults);
          }
        } 
else  {
          
for  ( int  i  =   0 ; i  <  skipResults; i ++ ) {
            
if  ( ! rs.next()) {
              
break ;
            }
          }
        }

        
//  Get Results
         int  resultsFetched  =   0 ;
        
while  ((maxResults  ==  SqlExecutor.NO_MAXIMUM_RESULTS  ||  resultsFetched  <  maxResults)  &&  rs.next()) {
          Object[] columnValues 
=  resultMap.resolveSubMap(request, rs).getResults(request, rs);
          callback.handleResultObject(request, columnValues, rs);
          resultsFetched
++ ;
        }
      }
    } 
finally  {
      request.setResultSet(
null );
    }
  }

 

此处优先使用的是ResultSet的absolute方法定位记录,是否支持absolute取决于具体数据库驱动,但一般当前版本的数据库都支持该方法,如果不支持则逐条跳过前面的记录。由此可以看出如果数据库支持absolute,则ibatis内置的分页策略与特定数据库的物理分页效率差距就在于物理分页查询与不分页查询在数据库中的执行效率的差距了。因为查询执行后读取数据前数据库并未把结果全部返回到内存,所以本身在存储占用上应该差距不大,如果都使用索引,估计执行速度也差不太多。

继续我们的话题。其实只要在executeQuery执行前组装sql,然后将其传给executeQuery,并告诉handleResults我们不需要逻辑分页即可。拦截executeQuery可以采用aop动态实现,也可直接继承SqlExecutor覆盖executeQuery来静态地实现,相比之下后者要简单许多,而且由于SqlExecutor没有实现任何接口,比较易变,动态拦截反到增加了维护的工作量,所以我们下面来覆盖executeQuery:

package  com.aladdin.dao.ibatis.ext;

import  java.sql.Connection;
import  java.sql.SQLException;

import  org.apache.commons.logging.Log;
import  org.apache.commons.logging.LogFactory;

import  com.aladdin.dao.dialect.Dialect;
import  com.ibatis.sqlmap.engine.execution.SqlExecutor;
import  com.ibatis.sqlmap.engine.mapping.statement.RowHandlerCallback;
import  com.ibatis.sqlmap.engine.scope.RequestScope;

public   class  LimitSqlExecutor  extends  SqlExecutor {

    
private   static   final  Log logger  =  LogFactory.getLog(LimitSqlExecutor. class );
    
    
private  Dialect dialect;

    
private   boolean  enableLimit  =   true ;

    
public  Dialect getDialect() {
        
return  dialect;
    }

    
public   void  setDialect(Dialect dialect) {
        
this .dialect  =  dialect;
    }

    
public   boolean  isEnableLimit() {
        
return  enableLimit;
    }

    
public   void  setEnableLimit( boolean  enableLimit) {
        
this .enableLimit  =  enableLimit;
    }

    @Override
    
public   void  executeQuery(RequestScope request, Connection conn, String sql,
            Object[] parameters, 
int  skipResults,  int  maxResults,
            RowHandlerCallback callback) 
throws  SQLException {
        
if  ((skipResults  !=  NO_SKIPPED_RESULTS  ||  maxResults  !=  NO_MAXIMUM_RESULTS)
                
&&  supportsLimit()) {
            sql 
=  dialect.getLimitString(sql, skipResults, maxResults);
            
if (logger.isDebugEnabled()){
                logger.debug(sql);
            }
            skipResults 
=  NO_SKIPPED_RESULTS;
            maxResults 
=  NO_MAXIMUM_RESULTS;            
        }
        
super .executeQuery(request, conn, sql, parameters, skipResults,
                maxResults, callback);
    }

    
public   boolean  supportsLimit() {
        
if  (enableLimit  &&  dialect  !=   null ) {
            
return  dialect.supportsLimit();
        }
        
return   false ;
    }

}

其中:

skipResults  =  NO_SKIPPED_RESULTS;
maxResults 
=  NO_MAXIMUM_RESULTS;

告诉handleResults不分页(我们组装的sql已经使查询结果是分页后的结果了),此处引入了类似hibenate中的数据库方言接口Dialect,其代码如下:

package  com.aladdin.dao.dialect;

public   interface  Dialect {
    
    
public   boolean  supportsLimit();

    
public  String getLimitString(String sql,  boolean  hasOffset);

    
public  String getLimitString(String sql,  int  offset,  int  limit);
}

 

下面为Dialect接口的MySQL实现: 

package  com.aladdin.dao.dialect;

public   class  MySQLDialect  implements  Dialect {

    
protected   static   final  String SQL_END_DELIMITER  =   " ; " ;

    
public  String getLimitString(String sql,  boolean  hasOffset) {
        
return   new  StringBuffer(sql.length()  +   20 ).append(trim(sql)).append(
                hasOffset 
?   "  limit ?,? "  :  "  limit ? " )
                .append(SQL_END_DELIMITER).toString();
    }

    
public  String getLimitString(String sql,  int  offset,  int  limit) {
        sql 
=  trim(sql);
        StringBuffer sb 
=   new  StringBuffer(sql.length()  +   20 );
        sb.append(sql);
        
if  (offset  >   0 ) {
            sb.append(
"  limit  " ).append(offset).append( ' , ' ).append(limit)
                    .append(SQL_END_DELIMITER);
        } 
else  {
            sb.append(
"  limit  " ).append(limit).append(SQL_END_DELIMITER);
        }
        
return  sb.toString();
    }

    
public   boolean  supportsLimit() {
        
return   true ;
    }

    
private  String trim(String sql) {
        sql 
=  sql.trim();
        
if  (sql.endsWith(SQL_END_DELIMITER)) {
            sql 
=  sql.substring( 0 , sql.length()  -   1
                    
-  SQL_END_DELIMITER.length());
        }
        
return  sql;
    }

}

接下来的工作就是把LimitSqlExecutor注入ibatis中。我们是通过spring来使用ibatis的,所以在我们的dao基类中执行注入,代码如下:

package  com.aladdin.dao.ibatis;

import  java.io.Serializable;
import  java.util.List;

import  org.springframework.orm.ObjectRetrievalFailureException;
import  org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import  com.aladdin.dao.ibatis.ext.LimitSqlExecutor;
import  com.aladdin.domain.BaseObject;
import  com.aladdin.util.ReflectUtil;
import  com.ibatis.sqlmap.client.SqlMapClient;
import  com.ibatis.sqlmap.engine.execution.SqlExecutor;
import  com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;

public   abstract   class  BaseDaoiBatis  extends  SqlMapClientDaoSupport {

    
private  SqlExecutor sqlExecutor;

    
public  SqlExecutor getSqlExecutor() {
        
return  sqlExecutor;
    }

    
public   void  setSqlExecutor(SqlExecutor sqlExecutor) {
        
this .sqlExecutor  =  sqlExecutor;
    }

    
public   void  setEnableLimit( boolean  enableLimit) {
        
if  (sqlExecutor  instanceof  LimitSqlExecutor) {
            ((LimitSqlExecutor) sqlExecutor).setEnableLimit(enableLimit);
        }
    }

    
public   void  initialize()  throws  Exception {
        
if  (sqlExecutor  !=   null ) {
            SqlMapClient sqlMapClient 
=  getSqlMapClientTemplate()
                    .getSqlMapClient();
            
if  (sqlMapClient  instanceof  ExtendedSqlMapClient) {
                ReflectUtil.setFieldValue(((ExtendedSqlMapClient) sqlMapClient)
                        .getDelegate(), 
" sqlExecutor " , SqlExecutor. class ,
                        sqlExecutor);
            }
        }
    }

    ...

}

 

其中的initialize方法执行注入,稍后会看到此方法在spring Beans 配置中指定为init-method。由于sqlExecutor是com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient的私有成员,且没有公开的set方法,所以此处通过反射绕过java的访问控制,下面是ReflectUtil的实现代码:

package  com.aladdin.util;

import  java.lang.reflect.Field;
import  java.lang.reflect.Method;
import  java.lang.reflect.Modifier;

import  org.apache.commons.logging.Log;
import  org.apache.commons.logging.LogFactory;

public   class  ReflectUtil {

    
private   static   final  Log logger  =  LogFactory.getLog(ReflectUtil. class );

    
public   static   void  setFieldValue(Object target, String fname, Class ftype,
            Object fvalue) {
        
if  (target  ==   null
                
||  fname  ==   null
                
||   "" .equals(fname)
                
||  (fvalue  !=   null   &&   ! ftype.isAssignableFrom(fvalue.getClass()))) {
            
return ;
        }
        Class clazz 
=  target.getClass();
        
try  {
            Method method 
=  clazz.getDeclaredMethod( " set "
                    
+  Character.toUpperCase(fname.charAt( 0 ))
                    
+  fname.substring( 1 ), ftype);
            
if  ( ! Modifier.isPublic(method.getModifiers())) {
                method.setAccessible(
true );
            }
            method.invoke(target, fvalue);

        } 
catch  (Exception me) {
            
if  (logger.isDebugEnabled()) {
                logger.debug(me);
            }
            
try  {
                Field field 
=  clazz.getDeclaredField(fname);
                
if  ( ! Modifier.isPublic(field.getModifiers())) {
                    field.setAccessible(
true );
                }
                field.set(target, fvalue);
            } 
catch  (Exception fe) {
                
if  (logger.isDebugEnabled()) {
                    logger.debug(fe);
                }
            }
        }
    }
}

 

到此剩下的就是通过Spring将sqlExecutor注入BaseDaoiBatis中了,下面是Spring Beans配置文件:

<? xml version="1.0" encoding="UTF-8" ?>
<! DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
    "http://www.springframework.org/dtd/spring-beans.dtd"
>

< beans >
    
<!--  Transaction manager for a single JDBC DataSource  -->
    
< bean  id ="transactionManager"  class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        
< property  name ="dataSource" >
            
< ref  bean ="dataSource"   />
        
</ property >
    
</ bean >
    
    
<!--  SqlMap setup for iBATIS Database Layer  -->
    
< bean  id ="sqlMapClient"  class ="org.springframework.orm.ibatis.SqlMapClientFactoryBean" >
        
< property  name ="configLocation" >
            
< value > classpath:/com/aladdin/dao/ibatis/sql-map-config.xml </ value >
        
</ property >
        
< property  name ="dataSource" >
            
< ref  bean ="dataSource"   />
        
</ property >
    
</ bean >

    
< bean  id ="sqlExecutor"  class ="com.aladdin.dao.ibatis.ext.LimitSqlExecutor" >
        
< property  name ="dialect" >
            
< bean  class ="com.aladdin.dao.dialect.MySQLDialect"   />
        
</ property >
    
</ bean >
    
    
< bean  id ="baseDao"  abstract ="true"  class ="com.aladdin.dao.ibatis.BaseDaoiBatis"  init-method ="initialize" >
        
< property  name ="dataSource" >
            
< ref  bean ="dataSource"   />
        
</ property >
        
< property  name ="sqlMapClient" >
            
< ref  bean ="sqlMapClient"   />
        
</ property >
        
< property  name ="sqlExecutor" >
            
< ref  bean ="sqlExecutor"   />
        
</ property >  
    
</ bean >  
    
    
< bean  id ="userDao"  class ="com.aladdin.dao.ibatis.UserDaoiBatis"  parent ="baseDao"   />  

    
< bean  id ="roleDao"  class ="com.aladdin.dao.ibatis.RoleDaoiBatis"  parent ="baseDao"   />
    
    
< bean  id ="resourceDao"  class ="com.aladdin.dao.ibatis.ResourceDaoiBatis"  parent ="baseDao"   />
    
</ beans >

 

此后就可以通过调用org.springframework.orm.ibatis.SqlMapClientTemplate的

public List queryForList(final String statementName, final Object parameterObject, final int skipResults, final int maxResults)   throws DataAccessException

public PaginatedList queryForPaginatedList(final String statementName, final Object parameterObject, final int pageSize)   throws DataAccessException

得到分页结果了。建议使用第一个方法,第二个方法返回的是PaginatedList,虽然使用简单,但是其获得指定页的数据是跨过我们的dao直接访问ibatis的,不方便统一管理。



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1408398

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值