前言
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;
}
@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对象中拿到上次执行的分页数据,直接放入返回参即可。
小结
至此升级版分页插件和使用就此完成,上面代码其实也只是我自己项目的一小部分而已,起到的也只是一个抛砖引玉的作用,欢迎大家在评论区与我讨论交流,我会尝试将这个插件做得更好更加优雅。