mybatis-plus 基于分页插件[PaginationInterceptor] 自定义[分页查询 总数查询]Sql位置

1. 场景

  1. 原有sql分页查询现在需要关联其他表格,数据结构为一对多。
    例如:一个老师有多个学生,老师列表已实现分页查询,需要关联查询一个老师下所有学生。
  2. 由于查询老师列表的sql里面有聚合函数,而且查询sql中字段还包含子查询。导致无法将关联的学生信息表做左连接查询。只能在查询出老师列表后,对结果做左连接查询
  3. 其次,对老师列表分页查询,如果关联了学生,就会导致老师列表分页不准确

2. 问题

  1. 由于mybatis plus目前实现的分页逻辑,都是在原有sql后面拼接分页逻辑。无法做到自定义分页查询逻辑拼接位置

3. 分析PaginationInterceptor源码

  1. 接口 ISqlParser 可实现自定义总数查询sql ,符合预期
  2. 数据库方言接口 IDialect 也能达到自定义分页查询sql,但是源码里面获取 IDialect实例的方法,无法利用装饰模式增强 IDialect实例的功能。
    总结:只能对整个拦截插件做修改,灵活度更高

4. 解决方案

  1. 插件源码

package xxx.xxx.xxx.xxx;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.parser.ISqlParser;
import com.baomidou.mybatisplus.core.parser.SqlInfo;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.baomidou.mybatisplus.extension.toolkit.SqlParserUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.springframework.util.Assert;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 分页拦截器
 *
 * @author hubin
 * @since 2016-01-23
 */
@Setter
@Accessors(chain = true)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class CustomPaginationInterceptor extends PaginationInterceptor implements Interceptor {

    protected static final Log logger = LogFactory.getLog(CustomPaginationInterceptor.class);

    /**
     * COUNT SQL 解析
     */
    private ISqlParser countSqlParser;

    /**
     * 溢出总页数,设置第一页
     */
    private boolean overflow = false;
    /**
     * 单页限制 500 条,小于 0 如 -1 不受限制
     */
    private long limit = 500L;
    /**
     * 方言类型
     */
    private String dialectType;
    /**
     * 方言实现类<br>
     * 注意!实现 com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect 接口的子类
     */
    private String dialectClazz;


    /**
     * Physical Page Interceptor for all the queries with parameter {@link RowBounds}
     */
    @SuppressWarnings("unchecked")
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);


        // SQL 解析
        this.sqlParser(metaObject);

        // 先判断是不是SELECT操作  (2019-04-10 00:37:31 跳过存储过程)
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
                || StatementType.CALLABLE == mappedStatement.getStatementType()) {
            return invocation.proceed();
        }

        // 针对定义了rowBounds,做为mapper接口方法的参数
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        Object paramObj = boundSql.getParameterObject();

        // 判断参数里是否有page对象
        IPage<?> page = null;
        if (paramObj instanceof IPage) {
            page = (IPage<?>) paramObj;
        } else if (paramObj instanceof Map) {
            for (Object arg : ((Map<?, ?>) paramObj).values()) {
                if (arg instanceof IPage) {
                    page = (IPage<?>) arg;
                    break;
                }
            }
        }

        /*
         * 不需要分页的场合,如果 size 小于 0 返回结果集
         */
        if (null == page || page.getSize() < 0) {
            String sql = boundSql.getSql();
            boolean isReplaceSql=false;
            if(sql.contains(CustomPaginationPosition.startIdentify)){
                sql=sql.replaceAll(CustomPaginationPosition.startIdentifyPattern, "");
                isReplaceSql=true;
            }
            if(sql.contains(CustomPaginationPosition.endIdentify)){
                sql=sql.replaceAll(CustomPaginationPosition.endIdentifyPattern, "");
                isReplaceSql=true;
            }
            if (isReplaceSql) {
                metaObject.setValue("delegate.boundSql.sql", sql);
            }
            return invocation.proceed();
        }

        /*
         * 处理单页条数限制
         */
        if (limit > 0 && limit <= page.getSize()) {
            page.setSize(limit);
        }


        Connection connection = (Connection) invocation.getArgs()[0];
        DbType dbType = StringUtils.isNotEmpty(dialectType) ? DbType.getDbType(dialectType)
                : JdbcUtils.getDbType(connection.getMetaData().getURL());
        Configuration configuration = mappedStatement.getConfiguration();
        CustomPaginationPosition customPaginationPosition = new CustomPaginationPosition(configuration,boundSql, page, dbType);


        String pageSql = customPaginationPosition.getPageSql();
        if (page.isSearchCount()) {
            SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), countSqlParser, pageSql);
            this.queryTotal(overflow, sqlInfo.getSql(), mappedStatement, customPaginationPosition.getPageBoundSql(), page, connection);
            if (page.getTotal() <= 0) {
                return null;
            }
        }

        DialectModel model = customPaginationPosition.parseDialectModel();

        List<ParameterMapping> mappings = new ArrayList<>(boundSql.getParameterMappings());
        Map<String, Object> additionalParameters = (Map<String, Object>) metaObject.getValue("delegate.boundSql.additionalParameters");
        model.consumers(mappings, configuration, additionalParameters);
        metaObject.setValue("delegate.boundSql.sql", customPaginationPosition.getPaginationSql());
        metaObject.setValue("delegate.boundSql.parameterMappings", mappings);
        return invocation.proceed();
    }

    private class CustomPaginationPosition {
        private static final String startIdentifyPattern = "\\^\\^\\{CustomPaginationPositionStartIdentify\\}\\^\\^";
        private static final String startIdentify = "^^{CustomPaginationPositionStartIdentify}^^";
        private static final String endIdentifyPattern = "\\^\\^\\{CustomPaginationPositionEndIdentify\\}\\^\\^";
        private static final String endIdentify = "^^{CustomPaginationPositionEndIdentify}^^";
        /**
         * 原始boundSql
         */
        private BoundSql boundSql;
        /**
         * 构造PageBoundSql 需要
         */
        private Configuration configuration;
        /**
         * 截取自定义分页开始位置 之前的sql
         */
        private String startSql;
        /**
         * 之前sql中有多少个 '?' 参数
         */
        private int startSqlParameterNum;
        /**
         * 自定义的 分页sql
         */
        @Getter
        private String pageSql;
        /**
         * 分页sql中参数个数
         */
        private int pageSqlParameterNum;
        /**
         * 同理 同上
         */
        private String endSql;
        /**
         * 同理 同上
         */
        private int endSqlParameterNum;

        /**
         * 构造分页sql对象 借助原始boundSql
         * 1.分页时
         * 2.不分页时
         */
        @Getter
        private BoundSql pageBoundSql;

        /**
         * 分页参数
         */
        private IPage<?> page;
        /**
         * sql方言类型
         */
        private DbType dbType;

        @Getter
        private DialectModel model;

        /**
         * 是否为分页sql  true 分页 false 不分页
         */
        @Getter
        private boolean isPaging;

        public CustomPaginationPosition(Configuration configuration,BoundSql boundSql, IPage<?> page, DbType dbType) {
            Assert.notNull(boundSql, "boundSql not null");
            Assert.notNull(boundSql, "configuration not null");
            this.boundSql = boundSql;
            this.configuration = configuration;
            String originalSql = boundSql.getSql();
            Assert.isTrue(org.apache.commons.lang3.StringUtils.isNotBlank(originalSql), "sql not null");
            int startIdentifyIndex = originalSql.indexOf(startIdentify);
            int endIdentifyIndex = originalSql.indexOf(endIdentify);

            Assert.isTrue((startIdentifyIndex == -1 && endIdentifyIndex == -1)
                    || (startIdentifyIndex != -1 && endIdentifyIndex != -1), String.format("自定义分页标识必须成对出现[(%s) - (%s)]", startIdentify, endIdentify));

            if (startIdentifyIndex != -1 && endIdentifyIndex != -1) {
                Assert.isTrue(startIdentifyIndex < endIdentifyIndex, String.format("[%s]标签必须在[%s]标签前面", startIdentify, endIdentify));
                this.startSql = originalSql.substring(0, startIdentifyIndex);
                this.pageSql = originalSql.substring(startIdentifyIndex + startIdentify.length(), endIdentifyIndex);
                this.endSql = originalSql.substring(endIdentifyIndex + endIdentify.length());
            } else {
                this.pageSql = originalSql;
            }
            if (org.apache.commons.lang3.StringUtils.isBlank(this.startSql)) {
                this.startSql = "";
            }
            if (org.apache.commons.lang3.StringUtils.isBlank(this.endSql)) {
                this.endSql = "";
            }
            //是否分页
            this.isPaging=!(null == page || page.getSize() < 0);

            if (this.isPaging) {
                this.startSqlParameterNum = org.apache.commons.lang3.StringUtils.countMatches(this.startSql, "?");
                this.pageSqlParameterNum = org.apache.commons.lang3.StringUtils.countMatches(this.pageSql, "?");
                this.endSqlParameterNum = org.apache.commons.lang3.StringUtils.countMatches(this.endSql, "?");

                this.pageBoundSql=new BoundSql(this.configuration,this.pageSql,this.boundSql.getParameterMappings(),this.boundSql.getParameterObject()){
                    BoundSql boundSql=CustomPaginationPosition.this.boundSql;
                    @Override
                    public String getSql() {
                        return boundSql.getSql();
                    }

                    @Override
                    public List<ParameterMapping> getParameterMappings() {
                        return boundSql.getParameterMappings().subList(CustomPaginationPosition.this.startSqlParameterNum
                                ,CustomPaginationPosition.this.startSqlParameterNum+CustomPaginationPosition.this.pageSqlParameterNum);
                    }

                    @Override
                    public Object getParameterObject() {
                        return boundSql.getParameterObject();
                    }

                    @Override
                    public boolean hasAdditionalParameter(String name) {
                        return boundSql.hasAdditionalParameter(name);
                    }

                    @Override
                    public void setAdditionalParameter(String name, Object value) {
                        boundSql.setAdditionalParameter(name, value);
                    }

                    @Override
                    public Object getAdditionalParameter(String name) {
                        return boundSql.getAdditionalParameter(name);
                    }
                };

            }else{
                this.pageBoundSql=new BoundSql(this.configuration,this.startSql+this.pageSql+this.endSql,this.boundSql.getParameterMappings(),this.boundSql.getParameterObject()){
                    BoundSql boundSql=CustomPaginationPosition.this.boundSql;

                    @Override
                    public List<ParameterMapping> getParameterMappings() {
                        return boundSql.getParameterMappings();
                    }

                    @Override
                    public Object getParameterObject() {
                        return boundSql.getParameterObject();
                    }

                    @Override
                    public boolean hasAdditionalParameter(String name) {
                        return boundSql.hasAdditionalParameter(name);
                    }

                    @Override
                    public void setAdditionalParameter(String name, Object value) {
                        boundSql.setAdditionalParameter(name, value);
                    }

                    @Override
                    public Object getAdditionalParameter(String name) {
                        return boundSql.getAdditionalParameter(name);
                    }
                };
            }

            this.page = page;
            this.dbType = dbType;


        }


        public String getPaginationSql() {
            return this.startSql + model.getDialectSql() + this.endSql;
        }

        public DialectModel parseDialectModel(){
            this.pageSql=concatOrderBy(this.pageSql, page);
            this.model = DialectFactory.buildPaginationSql(page, this.pageSql, dbType, dialectClazz);
            return this.model;
        }


    }



}

  1. 使用示例
    2.1 通过如下方式,即可在查询老师的列表信息中实现分页 总数 查询,并获取每个老师下所有学生

    <select id="xxx" resultMap="xxx">
        select
        *
        from 
        (
    	    ^^{CustomPaginationPositionStartIdentify}^^
    	    SELECT
    		*
    	    FROM teacher
    	    ${ew.customSqlSegment}
    	    ^^{CustomPaginationPositionEndIdentify}^^
        ) t
    
        LEFT JOIN student on s.tid= t.id
    </select>
    

5. 总结

  1. 当前插件的实现方式可能存在BUG,留待以后解决。欢迎各位探讨
    1.1 2020年9月23日17:43:38 更新自定义总数sql中存在参数时,参数位置不一致
    1.2 2020年10月23日09:22:35 更新分页sql在不执行分页查询时【例如:Page.size<0】,由于定义了标识符导致sql执行失败
  2. 官方能实现分页逻辑位置可自定义
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值