写在前面
由于目前的开发使用的ORM框架是MyBatis,作为一款优秀的半自动化ORM映射框架,Mybatis提供了灵活的sql语句的编写方式,但是过于灵活也使得所有的语句都需要自定义编写,例如通用的CURD操作也要从头编写一遍实际上从这一方面来看也会降低开发效率。而MyBatis-Plus就是为了简化Mybatis而生的。从名字不难看出Mybatis-Plus是Mybatis的加强版,它只对Mybatis的上层封装而不侵入现有工程,简单的配置可以让我们快速够建CRUD,其中分页功能更是极大的提高了开发效率。
点击查看MyBatis-Plus官网
快速使用
对于SpringBoot工程使用MyBatis-Plus非常简单,只需要在pom文件引入以下依赖即可集成进来
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.3.2</version>
</dependency>
内置分页插件分析
因为项目中很多地方用到了分页查询来展示数据到前端,在使用过程中不免产生对Mybatis-Plus分页插件的好奇,它是如何不用任何sql语句就几行代码就完成了我的分页操作呢?
首先来看看官网提供的分页插件使用方法
//Spring boot方式
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
public interface UserMapper {//可以继承或者不继承BaseMapper
/**
* <p>
* 查询 : 根据state状态查询用户列表,分页显示
* </p>
*
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象)
* @param state 状态
* @return 分页对象
*/
IPage<User> selectPageVo(Page<?> page, Integer state);
}
从以上 两段代码可以看出
一个是configuration作为MybatisPlus的配置里面生产一个PaginationInterceptor 的bean
另一个则是定义一个Mapper声明一个分页查询的接口,其中的返回类型为IPage类,参数类型为Page类型和Integer类型
后面的Mapper实现和Service层调用此处不再赘述,可以直接查看官网分页插件
由此咱们来大概猜想一下,分页插件的关键在于Page类和PaginationInterceptor (分页拦截器)
那么好了为了验证我们的猜想让我们来看一下其中的源码吧
此拦截器的位置在mybatis-plus-extension包中
打开看一下发现里面有intercept方法
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler)PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
this.sqlParser(metaObject);
MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
if (SqlCommandType.SELECT == mappedStatement.getSqlCommandType() && StatementType.CALLABLE != mappedStatement.getStatementType()) {
BoundSql boundSql = (BoundSql)metaObject.getValue("delegate.boundSql");
Object paramObj = boundSql.getParameterObject();
IPage<?> page = null;
if (paramObj instanceof IPage) {
page = (IPage)paramObj;
} else if (paramObj instanceof Map) {
Iterator var8 = ((Map)paramObj).values().iterator();
while(var8.hasNext()) {
Object arg = var8.next();
if (arg instanceof IPage) {
page = (IPage)arg;
break;
}
}
}
if (null != page && page.getSize() >= 0L) {
if (this.limit > 0L && this.limit <= page.getSize()) {
this.handlerLimit(page);
}
String originalSql = boundSql.getSql();
Connection connection = (Connection)invocation.getArgs()[0];
if (page.isSearchCount() && !page.isHitCount()) {
SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), this.countSqlParser, originalSql);
this.queryTotal(sqlInfo.getSql(), mappedStatement, boundSql, page, connection);
if (page.getTotal() <= 0L) {
return null;
}
}
DbType dbType = (DbType)Optional.ofNullable(this.dbType).orElse(JdbcUtils.getDbType(connection.getMetaData().getURL()));
IDialect dialect = (IDialect)Optional.ofNullable(this.dialect).orElse(DialectFactory.getDialect(dbType));
String buildSql = concatOrderBy(originalSql, page);
DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
Configuration configuration = mappedStatement.getConfiguration();
List<ParameterMapping> mappings = new ArrayList(boundSql.getParameterMappings());
Map<String, Object> additionalParameters = (Map)metaObject.getValue("delegate.boundSql.additionalParameters");
model.consumers(mappings, configuration, additionalParameters);
metaObject.setValue("delegate.boundSql.sql", model.getDialectSql());
metaObject.setValue("delegate.boundSql.parameterMappings", mappings);
return invocation.proceed();
} else {
return invocation.proceed();
}
} else {
return invocation.proceed();
}
}
这大概就是sql语句的拦截器吧,查看其逻辑不难发现
IPage<?> page = null;
if (paramObj instanceof IPage) {
page = (IPage)paramObj;
} else if (paramObj instanceof Map) {
Iterator var8 = ((Map)paramObj).values().iterator();
while(var8.hasNext()) {
Object arg = var8.next();
if (arg instanceof IPage) {
page = (IPage)arg;
break;
}
}
}
在此处进行判断传入的参数是不是IPage类型,因为你要是分页你必须传一个Page类型的参数里面包含PageNo和PageSize
如果是Page类型并且pageSize大于0
if (null != page && page.getSize() >= 0L)
//此处判断是不是每页大于500,limit=500,大于500就取500
if (this.limit > 0L && this.limit <= page.getSize())
然后就是处理sql语句了
String originalSql = boundSql.getSql();
Connection connection = (Connection)invocation.getArgs()[0];
if (page.isSearchCount() && !page.isHitCount()) {
SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), this.countSqlParser, originalSql);
this.queryTotal(sqlInfo.getSql(), mappedStatement, boundSql, page, connection);
if (page.getTotal() <= 0L) {
return null;
}
}
SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), this.countSqlParser, originalSql);
这里就是将原始的sql语句originalSql封装成带count的语句
封装过程在SqlparserUtils中
public static SqlInfo getOptimizeCountSql(boolean optimizeCountSql, ISqlParser sqlParser, String originalSql) {
if (!optimizeCountSql) {
return SqlInfo.newInstance().setSql(getOriginalCountSql(originalSql));
} else {
if (null == COUNT_SQL_PARSER) {
if (null != sqlParser) {
COUNT_SQL_PARSER = sqlParser;
} else {
COUNT_SQL_PARSER = new JsqlParserCountOptimize();
}
}
return COUNT_SQL_PARSER.parser((MetaObject)null, originalSql);
}
}
跟随调试器最后走到了
return COUNT_SQL_PARSER.parser((MetaObject)null, originalSql);
进入parser查看进入JsqlParserCountOptimize类
public SqlInfo parser(MetaObject metaObject, String sql)
其实现了ISqlParser类的parser方法
public SqlInfo parser(MetaObject metaObject, String sql) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("JsqlParserCountOptimize sql=" + sql);
}
SqlInfo sqlInfo = SqlInfo.newInstance();
try {
Select selectStatement = (Select)CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect)selectStatement.getSelectBody();
Distinct distinct = plainSelect.getDistinct();
GroupByElement groupBy = plainSelect.getGroupBy();
List<OrderByElement> orderBy = plainSelect.getOrderByElements();
if (null == groupBy && CollectionUtils.isNotEmpty(orderBy)) {
plainSelect.setOrderByElements((List)null);
sqlInfo.setOrderBy(false);
}
Iterator var9 = plainSelect.getSelectItems().iterator();
while(var9.hasNext()) {
SelectItem item = (SelectItem)var9.next();
if (item.toString().contains("?")) {
return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
}
}
if (distinct == null && null == groupBy) {
List<Join> joins = plainSelect.getJoins();
if (this.optimizeJoin && CollectionUtils.isNotEmpty(joins)) {
boolean canRemoveJoin = true;
String whereS = (String)Optional.ofNullable(plainSelect.getWhere()).map(Object::toString).orElse("");
Iterator var12 = joins.iterator();
label54: {
String str;
String onExpressionS;
do {
if (!var12.hasNext()) {
break label54;
}
Join join = (Join)var12.next();
if (!join.isLeft()) {
canRemoveJoin = false;
break label54;
}
Table table = (Table)join.getRightItem();
str = (String)Optional.ofNullable(table.getAlias()).map(Alias::getName).orElse(table.getName()) + ".";
onExpressionS = join.getOnExpression().toString();
} while(!onExpressionS.contains("?") && !whereS.contains(str));
canRemoveJoin = false;
}
if (canRemoveJoin) {
plainSelect.setJoins((List)null);
}
}
plainSelect.setSelectItems(COUNT_SELECT_ITEM);
return sqlInfo.setSql(selectStatement.toString());
} else {
return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(selectStatement.toString()));
}
} catch (Throwable var17) {
return sqlInfo.setSql(SqlParserUtils.getOriginalCountSql(sql));
}
}
里面大概的逻辑就是提取原始sql并分解,去除查询的字段,保留查询条件,去除排序,最后填充count()计算行数
接着构建分页查询
DbType dbType = (DbType)Optional.ofNullable(this.dbType).orElse(JdbcUtils.getDbType(connection.getMetaData().getURL()));
IDialect dialect = (IDialect)Optional.ofNullable(this.dialect).orElse(DialectFactory.getDialect(dbType));
String buildSql = concatOrderBy(originalSql, page);
DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
- 明确数据库类型
- 确定数据库方言
- 构建查询语句
- 最后在DialectModel中可以看见sql变成了原始sql+limit ?,?这样的了
最后就是执行sql:
invocation.proceed()
执行的过程就不详细分析了,至此分页的原理就搞清楚了
总结
现在整个过程就很清楚了,让咱们总结一下
- 拦截器拦截sql语句以及查询参数
- 以Page类型为判断依据看看参数中是否包含Page类型参数
- 接着确定分页的参数包括对页大小做限制(每页最多500条)
- 传入原始sql构建一个count sql
- 根据数据库类型使用不同的数据库方言进行封装,此处mysql将原始sql封装加上limit达到分页效果
- 执行两个分页的sql并返回结果