前言
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 > #{startTime}</if><if test="endTime != null">and t.create_time < #{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;}@Overridepublic 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 没数据不会返回 0Object 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);}@Overridepublic 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 且不限制返回值则不构造分页sqlLong _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对象中拿到上次执行的分页数据,直接放入返回参即可。
小结
至此升级版分页插件和使用就此完成,上面代码其实也只是我自己项目的一小部分而已,起到的也只是一个抛砖引玉的作用,欢迎大家在评论区与我讨论交流,我会尝试将这个插件做得更好更加优雅。