輕量級封裝DbUtils&Mybatis之三MyBatis分頁

MyBatis假分頁

參考DefaultResultSetHandler的skipRows方法。html

舒適提示:部分代碼請參考輕量級封裝DbUtils&Mybatis之一律要java

解決方案

1)以前公司同事,亦師亦上司勇哥已經處理過度頁的邏輯:自定義一個包裝類包裝SqlSession,徹底開放SqlSession的各種訪問方法,直接可經過傳入RowBounds(包裝offset&limit)參數完成分頁邏輯。
2)參考mybatis-pagination項目。git

備註:由於我的但願不要和MyBatis原有的使用方法差別太大,儘可能減小自定義的處理,因此才總結本身的思路和想法,目標其實是但願保留MyBatis自定義mapper接口便可實現Jdbc訪問的特性。github

舒適提示:請下載上面提到的項目,並對MyBatis分頁處理有必定了解。spring

分頁處理解決方案

mybatis-pagination分頁處理分析

1)經過外置增長的排序和分頁選項,在mapper文件中配置排序選項,經過參數控制排序的條件,而在interceptor攔截時處理分頁
2)自定義Interceptor和Executor,修改了目標執行邏輯處理各種條件邏輯sql

優勢:功可以強大
缺點:自定義的內容偏多,實現過於複雜,不知是否會受到MyBatis升級的影響apache

現有解決方案

feature
1)目前暫不支持排序,後續考慮,但絕對不會考慮在mapper配置文件中定義排序條件
2)自定義Interceptor,不考慮將結果集列表包裝成Page對象,保證分頁邏輯和不分頁的邏輯成爲可選項,調用方法可共用
3)全部和查詢結果集無關但有用的返回結果都包裝成一個對象,存放到ThreadLocal
4)定義Mapper接口的分頁方法最後一個參數類型務必是Criteria(參考下文代碼實現),不然沒法提供分頁功能session

攔截器處理流程
1)獲取MetaObject,獲得MappedStatement和ParameterHandler
2)斷定stamentId是否匹配配置的表達式,mapper接口方法最後一個參數是否爲Criteria類型,不知足斷定則執行原有SQL邏輯,知足則執行實際分頁處理邏輯
3)實際分頁處理時,關閉原有的分頁設置,將分頁參數綁定到SQL上,並執行獲取總記錄數的方法mybatis

代碼呈上

測試樣例app

package org.wit.ff.jdbc;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.wit.ff.jdbc.dao.HomeTownDao;
import org.wit.ff.jdbc.query.Criteria;
import org.wit.ff.jdbc.result.CriteriaResultHolder;

/**
 * Created by F.Fang on 2015/11/19.
 */
@ContextConfiguration(locations = {"classpath:applicationContext-paging.xml"})
public class HomeTownDaoPagingTest extends AbstractJUnit4SpringContextTests {

    @Autowired
    private HomeTownDao homeTownDao;

    @Test
    public void testFind() {
        // Criteria對象包裝分頁條件.
        System.out.println(homeTownDao.find(1, new Criteria().page(1, 1)));
        //System.out.println(homeTownDao.find(1, null));
        // 從線程上下文中獲取總頁數,總記錄數等信息.
        try {
            System.out.println(CriteriaResultHolder.get());
        }finally {
            CriteriaResultHolder.remove();
        }
    }

}

HomeTownDao

package org.wit.ff.jdbc.dao;

import org.wit.ff.jdbc.model.HomeTown;
import org.wit.ff.jdbc.query.Criteria;

import java.util.List;

/**
 * Created by F.Fang on 2015/11/17.
 * Version :2015/11/17
 */
public interface HomeTownDao {
    List<HomeTown> find(int id,Criteria criteria);
}

Criteria

package org.wit.ff.jdbc.query;


/**
 * Created by F.Fang on 2015/11/19.
 * 後續可擴展排序參數.
 */
public class Criteria {

    private int pageNumber;

    private int pageSize;

    public Criteria page(int pageNumber, int pageSize){
        this.pageNumber = pageNumber;
        this.pageSize = pageSize;
        return this;
    }


    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getPageNumber() {
        return pageNumber;
    }

    public void setPageNumber(int pageNumber) {
        this.pageNumber = pageNumber;
    }
}

CriteriaResult

package org.wit.ff.jdbc.result;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

/**
 * Created by Yong.Huang.
 * Updated by F.Fang on 2015/11/19.
 */
public class CriteriaResult{

    private int pageNumber;

    private int pageSize;

    private long pageCount;

    private long totalCount;

    public CriteriaResult(int pageNumber, int pageSize, long totalCount) {
        this.pageNumber = pageNumber;
        this.pageSize = pageSize;
        this.totalCount = totalCount;
        if (pageSize != 0) {
            if (totalCount % pageSize == 0) {
                pageCount = totalCount / pageSize;
            } else {
                pageCount = totalCount / pageSize + 1;
            }
        }
    }

    public int getPageNumber() {
        return pageNumber;
    }

    public int getPageSize() {
        return pageSize;
    }

    public long getPageCount() {
        return pageCount;
    }

    public long getTotalCount() {
        return totalCount;
    }

    public boolean hasPrevPage() {
        return pageNumber > 1 && pageNumber <= pageCount;
    }

    public boolean hasNextPage() {
        return pageNumber < pageCount;
    }

    public boolean isFirstPage() {
        return pageNumber == 1;
    }

    public boolean isLastPage() {
        return pageNumber == pageCount;
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
    }
}

CriteriaResultHolder

package org.wit.ff.jdbc.result;

/**
 * Created by F.Fang on 2015/11/19.
 */
public class CriteriaResultHolder {

    private static final ThreadLocal<CriteriaResult> criteriaResult = new ThreadLocal<CriteriaResult>();

    private CriteriaResultHolder(){}

    public static CriteriaResult get() {
        return criteriaResult.get();
    }

    public static void set(CriteriaResult value){
        if(criteriaResult.get() == null && value!=null){
            criteriaResult.set(value);
        }
    }

    public static void remove(){
        criteriaResult.remove();
    }
}

Mapper定義

<select id="find" resultType="HomeTown" >
        select * from hometown
    </select>

mybatis.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings> 

    <plugins>
        <plugin interceptor="org.wit.ff.jdbc.paging.MysqlPagingInterceptor">
            <property name="statementRegex" value=".*find.*"/>
        </plugin>
    </plugins>

</configuration>

Spring配置文件

<?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:mybatis="http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd">


    <!-- 數據源,請自行修改 -->
    <bean id="dataSource"
          class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">
        <property name="driverClassName" value="${db.driverClass}"/>
        <property name="url" value="${db.jdbcUrl}"/>
        <property name="username" value="${db.user}"/>
        <property name="password" value="${db.password}"/>
    </bean>

    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis.xml"/>
        <!-- 制定路徑自動加載mapper配置文件 -->
        <property name="mapperLocations" value="classpath:mappers/*Dao.xml"/>

        <!-- 配置myibatis的settings http://mybatis.github.io/mybatis-3/zh/configuration.html#settings -->
        <property name="configurationProperties">
            <props>
                <prop key="cacheEnabled">true</prop>
            </props>
        </property>
        <!-- 類型別名是爲 Java 類型命名一個短的名字。 它只和 XML 配置有關, 只用來減小類徹底 限定名的多餘部分 -->
        <property name="typeAliasesPackage" value="org.wit.ff.jdbc.model"/>

    </bean>

    <mybatis:scan base-package="org.wit.ff.jdbc.dao"/>

</beans>

核心攔截器
若對MyBatis攔截器相關的內容有疑問,請自行谷歌or百度,好的資源太多

package org.wit.ff.jdbc.paging;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.RowBounds;
import org.wit.ff.jdbc.dialect.Dialect;
import org.wit.ff.jdbc.query.Criteria;
import org.wit.ff.jdbc.result.CriteriaResult;
import org.wit.ff.jdbc.result.CriteriaResultHolder;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Properties;

/**
 * Created by Yong.Huang
 * Updated by F.Fang on 2015/11/19.
 * Mybatis屬於假分頁 , 參考代碼: DefaultResultSetHandler執行方法鏈:
 * handleResultSets--> handleResultSet --> handleRowValues --> handleRowValuesForNestedResultMap
 * --> skipRows --> 執行 rs.absolute跳過記錄數, 實際執行的語句仍然是查詢了相同數量的記錄.
 */
public abstract class PagingInterceptor implements Interceptor {

    /**
     * regex匹配statementId.
     */
    protected String statementRegex;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 匹配攔截StatementId
        if (!mappedStatement.getId().matches(statementRegex)) {
            return invocation.proceed();
        }

        // 最後一個參數必須是Creteria.
        ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.resultSetHandler.parameterHandler");

        if (parameterHandler != null) {
            Object object = parameterHandler.getParameterObject();
            Object lastParam = null;
            // 若是有多個參數,取最後一個參數.
            if (object instanceof HashMap) {
                HashMap map = (HashMap) object;
                // 這個邏輯始終不太放心, 往後如有更好的實現再改.
                String key = "param"+String.valueOf(map.keySet().size()/2);
                lastParam = map.get(key);
            } else {
                lastParam = object;
            }

            // 參數必定要匹配Criteria類型
            if (lastParam == null || !(lastParam instanceof Criteria)) {
                return invocation.proceed();
            }

            Criteria criteria = (Criteria) lastParam;
            StatementHandler stamentHandler = (StatementHandler) invocation.getTarget();
            BoundSql boundSql = stamentHandler.getBoundSql();
            // 原始mapper文件中配置的Sql.
            String originSql = boundSql.getSql();

            int offSet = (criteria.getPageNumber() - 1) * criteria.getPageSize();
            // 實際的分頁sql.
            String pagingSql = getDialect().getLimitString(originSql, offSet, criteria.getPageSize());

            // 從新設置屬性.
            metaObject.setValue("delegate.boundSql.sql", pagingSql);
            metaObject.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
            metaObject.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);

            // 獲取鏈接參數.
            Connection connection = (Connection) invocation.getArgs()[0];
            // 總頁數.
            int totalCount = getTotalCount(connection, originSql, parameterHandler);
            // 填充各屬性值.
            CriteriaResult result = new CriteriaResult(criteria.getPageNumber(), criteria.getPageSize(), totalCount);
            CriteriaResultHolder.set(result);
        }
        // 執行SQL.
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        statementRegex = properties.getProperty("statementRegex");
    }

    public abstract Dialect getDialect();

    private int getTotalCount(Connection connection, String sql, ParameterHandler parameterHandler) throws SQLException {
        int result = 0;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            String countSql = getDialect().getCountString(sql);
            ps = connection.prepareStatement(countSql);
            parameterHandler.setParameters(ps);
            rs = ps.executeQuery();
            if (rs.next()) {
                result = rs.getInt(1);
            }
        } finally {
            if (ps != null) {
                ps.close();
            }
            if (rs != null) {
                rs.close();
            }
        }
        return result;
    }
}
package org.wit.ff.jdbc.paging;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;
import org.wit.ff.jdbc.dialect.Dialect;
import org.wit.ff.jdbc.dialect.db.MySQLDialect;

import java.sql.Connection;

/**
 * Created by F.Fang on 2015/11/19.
 */
@Intercepts(
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})
)
public class MysqlPagingInterceptor extends PagingInterceptor{
    private Dialect dialect = new MySQLDialect();

    @Override
    public Dialect getDialect() {
        return dialect;
    }
}

QA

相關文章
相關標籤/搜索