【原创】提升MybatisPlus分页便捷性,制作一个属于自己的分页插件,让代码更加优雅

前言

MybatisPlus的分页插件有一点非常不好,就是要传入一个IPage,别看这个IPage没什么大不了的,最多多写一两行代码,可这带来一个问题,即使用xml的查询没法直接取对象里面变量的值了,得@Param指定xml中的变量名才行,得写#{search.name},而不是#{name},这也太不优雅了!可以说是相当的不优雅!

我之前就想过一些办法解决这个问题,比如使用PageHelper,这玩意更不省心,Page只会被第一次查询消费,而在我项目中,分页有一大堆的前置查询,比如:权限查询(最多),是否存在类查询(较多),以及其他一些前置查询业务。这往往会使得PageHelper被提前消费,列表依旧返回所有内容,这问题经常让人猝不及防,让程序猿苦不堪言。因此PageHelper方案也被我放弃了,最终还是打算自己实现一个分页插件,替换MP自己的分页插件。

设计思路

作为一名曾经的Android前端程序猿,Context模式对我来说再熟悉不过了,可以说是形影不离,即将几乎所有页面要用到的信息都放置到Context(上下文)中,那我对于后端请求来说不也可以这么做吗?将所有接口请求以及过程相关信息放到Context创建的对象中,对象放到线程中,随用随取,只要拿到Context意味着拿到了一切,跟Android的Context一样!当然这玩意必须结合MP的分页插件和PageHelper的优点,避免其自身的缺陷。

效果展示

图上为Kotlin代码(Android程序猿必备),实现分页仅需2行,

第一行:开启分页,说明下一个请求是需要执行分页的

第二行:进行查询,结果返回的只是一个List!分页信息呢?全保存在Context对象中了。

返回结果如上图所示,为了节约服务器带宽,我这边的返回参数全部使用单个字母表示,其中p就是page信息,pn:pageNum,ps:pageSize,tc:totalCount,tp:totalPage

当然这玩意和PageHelper一样,只能负责一次分页查询,当然一个接口也只需要一次分页查询, 不服来辩!

直接上代码

代码分为前中后三个部分

前期:准备Context

准备Context阶段我是在Aspect中进行的,切面为Controller方法,在执行Controller方法前,初始化一个Context对象并将其放到map中,Key为当前Thread对象,Value为Context,这里的代码过于复杂,且涉及到token校验,这里我就不放完整的出来了,以免我的服务器遭到攻击。

        val context = Context()
        val thread = Thread.currentThread()
        threadContextMap[thread] = context

反正大概就这意思,Context中当然也包含了所有入参信息,包括了pageNum、pageSize、totalCount、totalPage等等。

中期:准备xml、分页插件

由于项目中大量查询都是基于xml的,包含很多子查询和join查询,不可能都用QueryWrapper查询,因此xml的简洁化是必须的。我这里用的示例查询xml为:

    <select id="findByList" resultType="com.itdct.server.admin.example.vo.ExampleListVo">
        select t.* from test_example as t
        <where>
            <if test="name != null and name != ''">and t.name = #{name}</if>
            <if test="number != null">and t.number = #{number}</if>
            <if test="keyword != null and keyword != ''">and t.name like concat('%',#{keyword},'%')</if>
            <if test="startTime != null">and t.create_time &gt; #{startTime}</if>
            <if test="endTime != null">and t.create_time &lt; #{endTime}</if>
        </where>
        <if test="orderBy == null">order by t.create_time desc</if>
        <if test="orderBy != null">order by ${orderBy}</if>
    </select>

查询的Mapper为:

    fun findByList(query: ExampleQo): List<ExampleListVo>

可以发现查询方法不包含任何@Param,<if>中的变量也没有xxx.fieldName,甚至用ctrl+左键点击#{变量}还能跳转到类中相应的成员变量,这就是我想要实现的效果。

然后就是分页插件了,这个插件我还是基于原来的MP的分页插件,只需要对其进行稍加修改即可为我所用。

package com.itdct.server.admin.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
import com.itdct.server.common.dto.Context;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

/**
 * @author DCT
 * @version 1.0
 * @date 2023/11/10 14:53:24
 * @description
 */
public class ContextPaginationInnerInterceptor extends PaginationInnerInterceptor {
    protected Map<Thread, Context> threadContextMap;

    public ContextPaginationInnerInterceptor(DbType dbType) {
        super(dbType);
    }

    public ContextPaginationInnerInterceptor(IDialect dialect) {
        super(dialect);
    }

    public ContextPaginationInnerInterceptor(DbType dbType, Map<Thread, Context> threadContextMap) {
        super(dbType);
        this.threadContextMap = threadContextMap;
    }

    @Override
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // INFO: DCT: 2023/12/5 获取到当前线程的上下文对象
        Context context = threadContextMap.get(Thread.currentThread());
        if (context == null) {
            return true;
        }

        // INFO: DCT: 2023/12/5 不启动分页直接跳过
        boolean startPage = context.isStartPage();
        if (!startPage) {
            return true;
        }

        // INFO: DCT: 2023/12/5 这个page就是MP的分页Page 
        Page page = context.getPage();
        if (page == null) {
            return true;
        }

        long size = page.getSize();
        if (size < 0) {
            return true;
        }

        // INFO: DCT: 2023/12/5 以下为原来的MP分页插件代码 
        BoundSql countSql;
        MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
        if (countMs != null) {
            countSql = countMs.getBoundSql(parameter);
        } else {
            countMs = buildAutoCountMappedStatement(ms);
            String countSqlStr = autoCountSql(page, boundSql.getSql());
            PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
            countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
            PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
        }

        CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
        List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
        long total = 0;
        if (CollectionUtils.isNotEmpty(result)) {
            // 个别数据库 count 没数据不会返回 0
            Object o = result.get(0);
            if (o != null) {
                total = Long.parseLong(o.toString());
            }
        }

        page.setTotal(total);
        long totalPage = total / page.getSize();
        if (total % page.getSize() != 0) {
            totalPage++;
        }
        page.setPages(totalPage);

        return continuePage(page);
    }

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Context context = threadContextMap.get(Thread.currentThread());
        if (context == null) {
            return;
        }

        boolean startPage = context.isStartPage();
        if (!startPage) {
            return;
        }

        // INFO: DCT: 2023/12/5 这个page就是MP的分页Page 
        Page page = context.getPage();
        if (page == null) {
            return;
        }

        long size = page.getSize();
        if (size < 0) {
            return;
        }

        // 处理 orderBy 拼接
        boolean addOrdered = false;
        String buildSql = boundSql.getSql();
        List<OrderItem> orders = page.orders();
        if (CollectionUtils.isNotEmpty(orders)) {
            addOrdered = true;
            buildSql = this.concatOrderBy(buildSql, orders);
        }

        // size 小于 0 且不限制返回值则不构造分页sql
        Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
        if (page.getSize() < 0 && null == _limit) {
            if (addOrdered) {
                PluginUtils.mpBoundSql(boundSql).sql(buildSql);
            }
            return;
        }

        handlerLimit(page, _limit);
        IDialect dialect = findIDialect(executor);

        final Configuration configuration = ms.getConfiguration();
        DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);

        List<ParameterMapping> mappings = mpBoundSql.parameterMappings();
        Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
        model.consumers(mappings, configuration, additionalParameter);
        mpBoundSql.sql(model.getDialectSql());
        mpBoundSql.parameterMappings(mappings);

        // INFO: DCT: 2023/12/5 利用完后置为false 
        context.setStartPage(false);
    }
}

完整代码如上面所示,其中绝大部分都是MP原来的分页插件里的代码,我只是对其稍加修改而已。

后期:返回给前端

有了Context对象真的可以为所欲为哦,successPage方法如下:

    public <T> RespPageVo<T> successPage(List<T> pageData) {
        Context context = getContext();
        Page page = context.getPage();
        if (page != null) {
            return new RespPageVo<T>(pageData, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
        } else {
            log.warn("page is null!");
            return new RespPageVo<T>(pageData, 0L, 0L, 0L, 0L);
        }

    }

    public Context getContext() {
        Context context = threadContextMap.get(Thread.currentThread());
        return context;
    }

处于BaseService的代码还是Java写的,没有全面Kotlin化,由于Context对象中存有MP的Page对象,因此可以直接从Page对象中拿到上次执行的分页数据,直接放入返回参即可。

小结

至此升级版分页插件和使用就此完成,上面代码其实也只是我自己项目的一小部分而已,起到的也只是一个抛砖引玉的作用,欢迎大家在评论区与我讨论交流,我会尝试将这个插件做得更好更加优雅。

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值