十、Mybatis之分页查询

(一)功能

    能够实现不加“limit *,*”也能实现分页查询。

(二)具体实现

    1.建立一个分页对象

package com.imooc.entity;

/**
 * @author 潘畅
 * @date 2018/5/10 20:12
 */
public class Page {
    /**
     * 总条数(传过来的,数据库查询)
     */
    private int totalNumber;
    /**
     * 当前页(传过来的)
     */
    private int currentPage;
    /**
     * 每页显示数量(已知)
     */
    private int pageNumber = 3;
    /**
     * 总页数(需计算)
     */
    private int totalPage;
    /**
     * 每页的开始条目对应的数据库中查询的偏移量(需计算)
     */
    private int dbIndex;
    /**
     * 每页需要从数据库中查询多少数据(其实就等于pageNumber)
     */
    private int dbNumber;

    public Page() {
    }

    /**
     * 计算及规范各项数据
     */
    private void count(){
        /**
         * 首先计算总页数
         */
        int totalPageTemp = totalNumber/pageNumber;
        int plus = (totalNumber % pageNumber) == 0 ? 0:1;
        totalPageTemp += plus;
        if (totalPageTemp <= 0){
            totalPageTemp = 1;
        }
        totalPage = totalPageTemp;

        /**
         * 规范当前页
         */
        if (currentPage < 1){
            currentPage = 1;
        }
        if (currentPage > totalPage){
            currentPage = totalPage;
        }

        /**
         * 计算每页的开始条目对应的数据库中查询的偏移量
         */
        dbIndex = (currentPage - 1) * pageNumber;
        /**
         * 每页需要从数据库中查询多少数据
         */
        dbNumber = pageNumber;
    }

    public int getTotalPage() {
        return totalPage;
    }

    public void setTotalNumber(int totalNumber) {
        this.totalNumber = totalNumber;
        /**
         *     currentPage在计算totalNumber之前已经知道了,所以计算好totalNumber之后,向Page中设置的时候,
         * 就可以触发“count()”方法了!
         */
        count();
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        this.currentPage = currentPage;
    }

    public int getPageNumber() {
        return pageNumber;
    }

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

    public void setTotalPage(int totalPage) {
        this.totalPage = totalPage;
    }

    public int getTotalNumber() {
        return totalNumber;
    }

    public int getDbIndex() {
        return dbIndex;
    }

    public void setDbIndex(int dbIndex) {
        this.dbIndex = dbIndex;
    }

    public int getDbNumber() {
        return dbNumber;
    }

    public void setDbNumber(int dbNumber) {
        this.dbNumber = dbNumber;
    }
}

    2.创建分页拦截器

package com.imooc.interceptor;

import com.imooc.entity.Page;
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.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;

/**
 * 实现的接口“Interceptor”是Mybatis带的,不是JDK原生的
 * 这个类的原理:
 *     这个拦截器主要实现拦截“特定的方法所对应的sql语句”,通过对sql语句进行拼接
 *“limit offset,number”,来实现分页查询。
 *     而Mybatis中获取sql语句的方法,就是“StatementHandler”接口下的“prepare”方法,
 * 参数就是“Connection、Integer”,即 Statement prepare(Connection var1, Integer var2)。
 *     以上就对应“PageInterceptor”(分页拦截器)上的注解。
 *     备注:@Intercepts()中是一个数组,记得外围要加“{}”
 * @author 潘畅
 * @date 2018/5/11 9:50
 */
@Intercepts({@Signature(type = StatementHandler.class ,method = "prepare",args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
    /**
     *     实现“Interceptor”接口需要实现的“intercept()、plugin()、setProperties()”
     * 三个方法的执行顺序?
     * 一、先调用setProperties()方法,获取拦截器注册时的属性
     * 二、再调用plugin()方法,过滤拦截对象
     * 三、最后调用intercept()方法,执行拦截的逻辑
     */


    /**
     *     这个方法,只有在“plugin(Object target)”方法中,返回target的代理对象
     * (关于如何触发target的代理,见方法说明),才会被执行。
     * @param invocation 通过该参数,可以获取被拦截的对象(也就是实现了“StatementHandler”
     *                   接口的对象),此时的对象已经过代理处理
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /**
         *     Invocation可以获取被拦截的对象(PS:被拦截的对象肯定实现了“StatementHandler”
         * 接口,所以可以进行强转)
         */
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        /**
         * 问题:
         *     StatementHandler接口的prepare()方法获取sql语句,但它是抽象方法,具体实现是
         * 由“BaseStatementHandler”类,该类有一个成员变量“MappedStatement”,它存放配置
         * 文件中的每条sql语句。通过“MappedStatement”可以获取到配置文件中sql语句的详细信息,
         * 但是“BaseStatementHandler”类中的“MappedStatement”是“protected”类型,没有get()
         * 方法,所以无法直接获取到“MappedStatement”。
         * 解决办法:
         *     Mybatis对“StatementHandler”进行了封装,通过“MetaObject”对象实现!此时“metaObject”
         * 就等价于“statementHandler”,通过“metaObject”就可以获取成员变量“MappedStatement”
         */
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());

        /**
         * 关于“delegate.mappedStatement”详解:
         *     “PageInterceptor”拦截到“StatementHandler”接口以后,首先访问的是
         * “RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
         * 然后再通过这个成员变量访问到“BaseStatementHandler”类,才可以继续 访问到成员
         * 变量“mappedStatement”,所以键值是“delegate.mappedStatement”(键的写法遵循OGNL表达式)
         */
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        //获取配置文件中sql语句的id
        String id = mappedStatement.getId();
        //拦截以“ByPage”结尾的id,只要符合该条件的“StatementHandler”才能继续执行(正则表达式)
        if (id.matches(".+ByPage$")){
            /**
             *     在“StatementHandler”接口的实现类“BaseStatementHandler”中的
             * “prepare()”方法中可以看到,是通过“this.boundSql.getSql()”获取sql
             * 语句的,所以我们要得到“boundSql”对象,“BoundSql”对象不同于
             * “MappedStatement”对象,它是有"get()"方法的,所以直接用“get()”方法获取即可。
             */
            BoundSql boundSql = statementHandler.getBoundSql();
            //获取原始sql语句(此时的sql语句中若有“id=#{id}”这样的参数,则已经被Mybatis转换成了“?”)
            String sql = boundSql.getSql();
            /**
             * 拼接分页sql语句
             * 问题:
             *     没有参数,“limit *,*”没法写?所以接下来,需要拿到传递过来的参数
             */
            /**
             *     通过boundSql获取参数,然后进行强转(PS:一般来说,分页查询最少需要用
             * 到一个Page对象和者其他对象,所以一般遇到分页查询,我们设置个规定,参数必
             * 须是map类型,取出Page对象的键值就是“page”)
             */
            Map<?, ?> params = (Map<?, ?>)boundSql.getParameterObject();
            Page page = (Page) params.get("page");
            /**
             * 问题:
             *     此时Service层传递过来的Page对象只有“currentPage”(当前页码),还缺少
             * 一个“totalNumber”(总条数),否则无法计算出“totalPage”(总页数),返回
             * 的“Page”对象是残缺的?(前端需要用到这个Page对象)
             * 解决办法:
             *     查询“totalNumber”(总条数),完善Page对象!
             */
            /**
             * 查询总条数的sql语句
             * 说明:
             *     我们从Mybatis中拿到的sql语句,就是目标sql语句(查询出我们想要的所有条目),
             * 所以,我们可以将拿到的sql语句作为一个子查询,计算总条目即可。
             */
            String totalNumberSql = "select count(*) from (" + sql + ")a";
            /**
             * 问题1:
             *     这里为什么用原生的JDK来执行“查询总条数”sql语句?
             * 原因:
             *     由于我们需要在sql放回Mybatis框架之前执行!
             *     因为在将改造后的“分页sql”放回Mybatis框架之前,我们必须保证Page对象是
             * 完整的,因为这个Page对象,在Mybatis框架处理之后,需要返回去的(我们前端需要
             * 这个Page对象)。所以这里我们只有先获取总条数,完善Page对象!
             * 问题2:
             *     使用JDK原生,则需要“Connection”对象,如何获取?
             * 解决:
             *     我们的“PageInterceptor”(分页过滤器)拦截的“StatementHandler.prepare(Connection var1, Integer var2)”
             * (PS:见注解和前面说明),里面的第一个参数就是“Connection”对象,可以通过
             * “Invocation”对象获取,明显“Connection”对象是第一个参数!
             */
            Connection connection = (Connection) invocation.getArgs()[0];
            /*--------以下就是执行sql语句,获取总条数--------*/

            /**
             * 问题:
             *     我们是将从Mybatis中拿到的sql语句作为子查询语句的,这样有一个问题,原
             * sql语句可能需要Mybatis框架将这样“ #{id}”(“id = #{id}”)转换成“?”号,
             * 并且记录“?”号和参数之间的对应关系(比如第一个“?”号对应哪一个参数),
             * 我们怎么确定这个对应关系?
             * 分析:
             *     Mybatis再将sql语句中的“#{id}”替换成“?”之后,肯定记录了“?”和参数的
             * 对应关系。而记录这些信息的类就是"ParameterHandler"接口。"ParameterHandler"接
             * 口中有一个方法“setParameters(PreparedStatement var1)”,就是根据Mybatis框架掌
             * 握的“?”和参数的对应关系,来将参数设置到“PreparedStatement”对象中。当然,这
             * 样做的前提是我们不能在新的语句中加入新的参数了,否则就破坏了"ParameterHandler"接
             * 口掌握的“?”和参数的对应关系。
             * 解决:
             *     获取“ParameterHandler”对象,将其掌握的“?”和参数的对应关系设置到PreparedStatement”对象中。
             * 参数说明:
             *     和前面类似,“PageInterceptor”拦截到“StatementHandler”接口以后,首先访问
             * 的是“RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
             * 然后再通过这个成员变量访问到“BaseStatementHandler”类,该类下有个成员变量
             * “ParameterHandler parameterHandler”,所以键值是“delegate.parameterHandler”(遵循OGNL表达式)
             */
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");

            PreparedStatement statement = connection.prepareStatement(totalNumberSql);
            /**
             *     调用ParameterHandler对象的“setParameters(PreparedStatement var1)”方法,
             * 将其掌握的“?”和参数的对应关系设置到PreparedStatement”对象中
             */
            parameterHandler.setParameters(statement);

            ResultSet resultSet = statement.executeQuery();
            //因为就1条数据,就不使用“while”了
            if (resultSet.next()){
                //列下标是从“1”开始
                page.setTotalNumber(resultSet.getInt(1));
            }
            /*--------以上就是执行sql语句,获取总条数--------*/

            String pageSql = sql + " LIMIT " + page.getDbIndex() + "," + page.getDbNumber();
            /**
             * 问题:
             *     我们从Mybatis中取出sql语句并进行改造,改造过后,我们应该再将sql语句放回去。
             * 但是,我们取sql语句有get()方法(boundSql.getSql()),却没有“set()”方法?
             * 解决:
             *     依然是通过上面的MetaObject对象来将sql语句放回去
             * 键值“delegate.boundSql.sql”说明:
             *    “PageInterceptor”拦截到“StatementHandler”接口以后,首先访问的是
             *“RoutingStatementHandler”类,该类下有个成员变量“StatementHandler delegate”,
             * 然后再通过这个成员变量访问到“BaseStatementHandler”类,该类下有个成员
             * 变量“BoundSql boundSql”,然后继续访问“BoundSql”类,该类下有个“String sql”
             * 对象,所以键值是“delegate.boundSql.sql”(遵循OGNL表达式)
             */
            //将改造后的sql语句再放回Mybatis中
            metaObject.setValue("delegate.boundSql.sql", pageSql);
        }
        /**
         *     invocation.proceed():通过反射继续执行获取SQL语句之后的代码(即Mybatis
         * 框架代为进行的执行sql语句,返回结果。。)
         */
        return invocation.proceed();

        //
        /**
         * 最后一步,拦截器一定一定要在mybatis总配置文件中注册该“拦截器”
         * <plugins>
         * <plugin interceptor="com.imooc.interceptor.PageInterceptor"/>
         * </plugins>
         */
    }

    /**
     * 方法的功能:判断被拦截的对象,是否需要进行某些处理
     * @param target 被拦截的对象
     * @return
     */
    @Override
    public Object plugin(Object target) {
        /**
         *     下面的意思是,若target满足了"PageInterceptor"(分页过滤器)的过滤
         * 要求,则对target进行代理,返回被代理对象,否则直接返回对象。(PS:对于
         * 分页过滤器而言,它的拦截条件就是,只要涉及到与数据库交互(获取SQL语句),
         * 都会被拦截)
         */
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        /**
         * 若注册拦截器时这样写:
         * <plugins>
         * <plugin interceptor="com.imooc.interceptor.PageInterceptor">
         * <property name="test" value="abc"/>
         * </plugin>
         * </plugins>
         * 那么就以用“properties.getProperty("test")”获取到值“abc”
         */
    }
}

    3.在Mybatis总配置文件中注册“分页拦截器”(千万不能忘记)

<plugins>
   <plugin interceptor="com.imooc.interceptor.PageInterceptor"/>
</plugins>

猜你喜欢

转载自blog.csdn.net/panchang199266/article/details/80288863