深入淺出Mybatis-分頁

Mr_Alexander發表於2016-07-05

Mybatis的分頁功能很弱,它是基於記憶體的分頁(查出所有記錄再按偏移量和limit取結果),在大資料量的情況下這樣的分頁基本上是沒有用的。本文基於外掛,通過攔截StatementHandler重寫sql語句,實現資料庫的物理分頁。本文適配的mybatis版本是3.2.2

準備

為什麼在StatementHandler攔截

深入淺出MyBatis-Sqlsession章節介紹了一次sqlsession的完整執行過程,從中可以知道sql的解析是在StatementHandler裡完成的,所以為了重寫sql需要攔截StatementHandler

MetaObject簡介

在我的實現裡大量使用了MetaObject這個物件,因此有必要先介紹下它。MetaObjectMybatis提供的一個的工具類,通過它包裝一個物件後可以獲取或設定該物件的原本不可訪問的屬性(比如那些私有屬性)。它有個三個重要方法經常用到:

1)       MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)

2)       Object getValue(String name)

3)       void setValue(String name, Object value)

方法1)用於包裝物件;方法2)用於獲取屬性的值(支援OGNL的方法);方法3)用於設定屬性的值(支援OGNL的方法);

外掛的原理

參見深入淺出Mybatis-外掛原理

 

有了上面這些基礎知識的準備後,就可以我們的主題了。

攔截器簽名

@Intercepts({@Signature(type =StatementHandler.class, method = "prepare", args ={Connection.class})})
publicclass PageInterceptor implementsInterceptor {
...
}

從簽名裡可以看出,要攔截的目標型別是StatementHandler(注意:type只能配置成介面型別),攔截的方法是名稱為prepare引數為Connection型別的方法。

intercept的實現

   public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler,
        DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
        // 分離代理物件鏈(由於目標類可能被多個攔截器攔截,從而形成多次代理,通過下面的兩次迴圈
        // 可以分離出最原始的的目標類)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, 
            DEFAULT_OBJECT_WRAPPER_FACTORY);
        }
        // 分離最後一個代理物件的目標類
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, 
            DEFAULT_OBJECT_WRAPPER_FACTORY);
        }
        Configuration configuration = (Configuration) metaStatementHandler.
        getValue("delegate.configuration");
        dialect = configuration.getVariables().getProperty("dialect");
        if (null == dialect || "".equals(dialect)) {
            logger.warn("Property dialect is not setted,use default 'mysql' ");
            dialect = defaultDialect;
        }
        pageSqlId = configuration.getVariables().getProperty("pageSqlId");
        if (null == pageSqlId || "".equals(pageSqlId)) {
            logger.warn("Property pageSqlId is not setted,use default '.*Page$' ");
            pageSqlId = defaultPageSqlId;
        }
        MappedStatement mappedStatement = (MappedStatement) 
        metaStatementHandler.getValue("delegate.mappedStatement");
        // 只重寫需要分頁的sql語句。通過MappedStatement的ID匹配,預設重寫以Page結尾的
        //  MappedStatement的sql
        if (mappedStatement.getId().matches(pageSqlId)) {
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("parameterObject is null!");
            } else {
                // 分頁引數作為引數物件parameterObject的一個屬性
                PageParameter page = (PageParameter) metaStatementHandler
                        .getValue("delegate.boundSql.parameterObject.page");
                String sql = boundSql.getSql();
                // 重寫sql
                String pageSql = buildPageSql(sql, page);
                metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
                // 採用物理分頁後,就不需要mybatis的記憶體分頁了,所以重置下面的兩個引數
                metaStatementHandler.setValue("delegate.rowBounds.offset", 
                RowBounds.NO_ROW_OFFSET);
                metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
                Connection connection = (Connection) invocation.getArgs()[0];
                // 重設分頁引數裡的總頁數等
                setPageParameter(sql, connection, mappedStatement, boundSql, page);
            }
        }
        // 將執行權交給下一個攔截器
        return invocation.proceed();
    }

StatementHandler的預設實現類是RoutingStatementHandler,因此攔截的實際物件是它。RoutingStatementHandler的主要功能是分發,它根據配置Statement型別建立真正執行資料庫操作的StatementHandler,並將其儲存到delegate屬性裡。由於delegate是一個私有屬性並且沒有提供訪問它的方法,因此需要藉助MetaObject的幫忙。通過MetaObject的封裝後我們可以輕易的獲得想要的屬性。

在上面的方法裡有個兩個迴圈,通過他們可以分離出原始的RoutingStatementHandler(而不是代理物件)。

前面提到,簽名裡配置的要攔截的目標型別是StatementHandler攔截的方法是名稱為prepare引數為Connection型別的方法,而這個方法是每次資料庫訪問都要執行的。因為我是通過重寫sql的方式實現分頁,為了不影響其他sql(update或不需要分頁的query),我採用了通過ID匹配的方式過濾。預設的過濾方式只對id以Page結尾的進行攔截(注意區分大小寫),如下:

	<select id="queryUserByPage" parameterType="UserDto" resultType="UserDto">
	    <![CDATA[
		select * from t_user t where t.username = #{username}
		]]>
	</select>

當然,也可以自定義攔截模式,在mybatis的配置檔案里加入以下配置項:

	<properties>
		<property name="dialect" value="mysql" />
		<property name="pageSqlId" value=".*Page$" />
	</properties>

其中,屬性dialect指示資料庫型別,目前只支援mysql和oracle兩種資料庫。其中,屬性pageSqlId指示攔截的規則,以正則方式匹配。

sql重寫

sql重寫其實在原始的sql語句上加入分頁的引數,目前支援mysql和oracle兩種資料庫的分頁。

    private String buildPageSql(String sql, PageParameter page) {
        if (page != null) {
            StringBuilder pageSql = new StringBuilder();
            if ("mysql".equals(dialect)) {
                pageSql = buildPageSqlForMysql(sql, page);
            } else if ("oracle".equals(dialect)) {
                pageSql = buildPageSqlForOracle(sql, page);
            } else {
                return sql;
            }
            return pageSql.toString();
        } else {
            return sql;
        }
    }

 mysql的分頁實現:

    public StringBuilder buildPageSqlForMysql(String sql, PageParameter page) {
        StringBuilder pageSql = new StringBuilder(100);
        String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
        pageSql.append(sql);
        pageSql.append(" limit " + beginrow + "," + page.getPageSize());
        return pageSql;
    }

oracle的分頁實現:

    public StringBuilder buildPageSqlForOracle(String sql, PageParameter page) {
        StringBuilder pageSql = new StringBuilder(100);
        String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
        String endrow = String.valueOf(page.getCurrentPage() * page.getPageSize());
        pageSql.append("select * from ( select temp.*, rownum row_id from ( ");
        pageSql.append(sql);
        pageSql.append(" ) temp where rownum <= ").append(endrow);
        pageSql.append(") where row_id > ").append(beginrow);
        return pageSql;
    }

分頁引數重寫

有時候會有這種需求,就是不但要查出指定頁的結果,還需要知道總的記錄數和頁數。我通過重寫分頁引數的方式提供了一種解決方案:

 

    /**
     * 從資料庫裡查詢總的記錄數並計算總頁數,回寫進分頁引數<code>PageParameter</code>,這樣呼叫 
     * 者就可用通過 分頁引數<code>PageParameter</code>獲得相關資訊。
     * 
     * @param sql
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param page
     */
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
            BoundSql boundSql, PageParameter page) {
        // 記錄總記錄數
        String countSql = "select count(0) from (" + sql + ") as total";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());
            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
            page.setTotalCount(totalCount);
            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
            page.setTotalPage(totalPage);
        } catch (SQLException e) {
            logger.error("Ignore this exception", e);
        } finally {
            try {
                rs.close();
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
            try {
                countStmt.close();
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
        }
    }

    /**
     * 對SQL引數(?)設值
     * 
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
            Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }

 

 

plugin的實現

    public Object plugin(Object target) {
        // 當目標類是StatementHandler型別時,才包裝目標類,否者直接返回目標本身,減少目標被代理的
        // 次數
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

原文連結:http://www.cnblogs.com/snake-hand/p/3177907.html