iBatis的分页分析与详解

编程技术  /  houtizong 发布于 3年前   65

        分页是操作数据库型系统常遇到的问题。分页实现方法很多,但效率的差异就很大了。iBatis是通过什么方式来实现这个分页的了。查看它的实现部分,发现返回的PaginatedList实际上是个接口,实现这个接口的是PaginatedDataList类的对象,查看PaginatedDataList类发现,每次翻页的时候最后都会调用下面这段函数。

private List getList(int idx, int localPageSize) throws SQLException {     return sqlMapExecutor.queryForList(statementName, parameterObject, (idx) * pageSize, localPageSize);   }

        由于

public interface SqlMapClient extends SqlMapExecutor, SqlMapTransactionManager {……} 

        所以实际的调用次序如下:

SqlMapClientImpl.queryForPaginatedList->SqlMapSessionImpl.queryForPaginatedList   ->SqlMapExecutorDelegate.queryForPaginatedList->GeneralStatement.executeQueryForList   ->GeneralStatment.executeQueryWithCallback->GeneralStatment.executeQueryWithCallback   ->SqlExecutor.executeQuery->SqlExecutor.handleMultipleResults()->SqlExecutor.executeQuery-> handleResults  

        iBATIS分页处理的函数如下:

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()) {               return;             }           }         }           // 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);     }   }

        从代码中可以看出iBatis分页查询的逻辑是首先判断ResulteSet的类型,如果ResultSet的类型是ResultSet.TYPE_FORWARD_ONLY,则使用ResultSet对象的next()方法,一步一步地移动游标到要取的第一条记录的位置,然后再采用next()方法取出一页的数据;如果ResultSet的类型不是ResultSet.TYPE_FORWARD_ONLY,则采用 ResultSet对象的absolute()方法,移动游标到要取的第一条记录的位置,然后再采用next()方法取出一页的数据。

        ResultSet的类型,是在iBatis的配置文件中配置的,如:

<select id="queryAllUser" resultMap="user" resultSetType="FORWARD_ONLY">select id,name from user_tab</select>

        其中resultSetType的可选值为FORWARD_ONLY | SCROLL_INSENSITIVE | SCROLL_SENSITIVE,如果没有配置,默认值为FORWARD_ONLY,FORWARD_ONLY类型的ResultSet 不支持absolute方法,所以是通过next方法定位的。一般情况下,我们都使用FORWARD_ONLY类型的ResultSet,SCROLL类 型ResultSet的优点是可向前,向后滚动,并支持精确定位(absolute),但缺点是把结果集全部加载进缓存(如果查询是从1000000条开 始取100条,会把前100万条数据也加载进缓存),容易造成内存溢出,性能也很差,除非必要,一般不使用。

        可见,iBatis的分页完全依赖于JDBC ResultSet的next方法或absolute方法来实现。

        所以分页还是要考虑采用直接操作sql语句来完成。当然,小批量的可以采用iBatis的分页模式。一般分页的sql语句与数据库的具体实现有关。

        mySql:

select * from A limit startRow,endRow;

        oracle:

select b.* from (select a.*,rownum as linenum from (select * from A) a where rownum <= endRow) b where linenum >= startRow

         而Hibernate在分页查询方面,比iBatis要好很多,Hibernate可以 根据不同的数据库,对sql做不同的优化加工,然后再执行优化后的sql。比如,对于Oracle数据库来说,原始sql为select * form user_tab, 从1000001条开始取100条,则hibernate加工后的sql为:

select *  from (select row_.*, rownum rownum_          from (SELECT * FROM user_tab) row_         where rownum <= 1000100) where rownum_ > 1000000

        Hibernate的Oracle分页采用的就是是拼凑RowNum的Sql语句来完成的。参考代码如下:

public String createOraclePagingSql(String sql, int pageIndex, int pageSize){       int m = pageIndex * pageSize;       int n = m + pageSize;       return "select * from ( select row_.*, rownum rownum_ from ( " + sql               + " ) row_ where rownum <= " + n                + ") where rownum_ > " + m;   }

        写一个程序,对比一下两种方式下的查询效率。程序如下:

package com.bijian.study;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;public class Test {        public static void main(String[] args) throws Exception {        Class.forName("oracle.jdbc.driver.OracleDriver");        Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:db", "username","password");        long a = System.currentTimeMillis();        testPageQuery1(conn);        //testPageQuery2(conn);          long b = System.currentTimeMillis();        System.out.println(b - a);    }    //共217760条记录,耗费71943毫秒    public static void testPageQuery1(Connection conn) throws Exception {        String sql = "SELECT * FROM test_table";        Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);        ResultSet rs = stmt.executeQuery(sql);        int j = 0;        //游标移动到217661条数据的位置          while (rs.next() && j++ < 217661) {        }        int i = 0;        //依次取出100条数据          while (rs.next() && i++ < 100) {        }    }        //共217760条记录,耗费6062毫秒    public static void testPageQuery2(Connection conn) throws Exception {                String sql = "SELECT * FROM test_table";                StringBuffer pagingSelect = new StringBuffer(sql.length() + 100);        pagingSelect.append("select * from ( select row_.*, rownum rownum_ from ( ");        pagingSelect.append(sql);        pagingSelect.append(" ) row_ where rownum <= 217760) where rownum_ > 217660");        Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);        ResultSet rs = stmt.executeQuery(pagingSelect.toString());        while (rs.next()) {        }    }}

        testPageQuery1方法是用ibatis采用的分页方法查询,testPageQuery2是用Hibernate采用的分页方法查询,发现testPageQuery1需要执行76秒,而testPageQuery2仅需要执行6~7秒,差异很大。而如果改成从1000条开始取100 条,甚至更靠前,则2者的差别是非常小的。

        综上所述,如果系统中查询的数据量很大,并且用户会选择查询非常靠后的数据,那么我们就应该替换iBatis的分页实现,如果不存在这种情况,那我们就不需要替换iBatis的分页实现,一般情况下,用户不可能去查询那么靠后的页,难道这也是iBatis一直不修改分页实现的原因?

        如果选择替换的话,想到如下四种办法:

        第一种方法:自己写一个类,继承iBatis的SqlExecutor,然后把这个类注入到 com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient中,由于SqlExecutor是 ExtendedSqlMapClient的私有变量,没有public类型的set方法,所以需要采用reflect机制注入;

        第二种方法:在自己的工程里写一个和iBatis的SqlExecutor的包名和类名完全一样的类,web工程中,WEB-INF/classes下的java类,先于 WEB-INF/lib下jar包的加载,所以就巧妙了覆盖了iBatis的SqlExecutor类;

        第三种方法:弃用iBatis的分页查询方法 queryForList(String sql,Object obj,int maxResult,int  skipResult),而用普通查询方法,queryForList(String sql,Object obj)。只不过把maxResult和skipResult都作为obj的变量传到sql里去。如下:

<select id="queryAllUser" parameterClass="java.util.Map" resultMap="user">select *  from (select row_.*, rownum rownum_      from (SELECT * FROM user_tab) row_     where rownum <= #_maxResult#)where rownum_ > #_skipResult#</select>

        第四种方法:调封装了分页功能的Oracle Package 

create or replace package body FMW_FY_HELPER is  PROCEDURE GET_DATA(pi_sql in varchar,pi_whichpage in integer,pi_rownum in integer,  po_cur_data out cur_DATA,po_allrownum out integer,pio_succeed in out integer)  as   v_cur_data cur_DATA;  v_cur_temp cur_TEMP;  v_temp integer;  v_sql varchar(5000);  v_temp1 integer;  v_temp2 integer;  begin  pio_succeed := 1;  v_sql := 'select count(''a'') from ( ' || pi_sql || ')';  execute immediate v_sql into v_temp;    po_allrownum:=ceil(v_temp/pi_rownum);    v_sql := '';  v_temp :=pi_whichpage*pi_rownum + 1;  v_temp1:=(pi_whichpage-1)*pi_rownum + 1;  v_temp2:=pi_whichpage*pi_rownum;  v_sql:= 'select * from (select rownum as rn,t.* from (' || pi_sql ||') t where rownum<' || to_char(v_temp) || ')  where rn between ' || to_char(v_temp1) || ' and ' || to_char(v_temp2);  open v_cur_data for v_sql;  if v_cur_data %notfound  then  pio_succeed:=-1;  return;  end if;po_cur_DATA := v_cur_data;end;
         附:引出的一个数据权限的问题

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!

留言需要登陆哦

技术博客集 - 网站简介:
前后端技术:
后端基于Hyperf2.1框架开发,前端使用Bootstrap可视化布局系统生成

网站主要作用:
1.编程技术分享及讨论交流,内置聊天系统;
2.测试交流框架问题,比如:Hyperf、Laravel、TP、beego;
3.本站数据是基于大数据采集等爬虫技术为基础助力分享知识,如有侵权请发邮件到站长邮箱,站长会尽快处理;
4.站长邮箱:[email protected];

      订阅博客周刊 去订阅

文章归档

文章标签

友情链接

Auther ·HouTiZong
侯体宗的博客
© 2020 zongscan.com
版权所有ICP证 : 粤ICP备20027696号
PHP交流群 也可以扫右边的二维码
侯体宗的博客