通过一次性能优化,再次记牢了mybatisplus的QueryWrapper、LambdaQueryWrapper、AbstractWrapper这三者之间的关系...

简介: 本文描述了一次针对SpringBoot应用中MyBatisPlus分页查询的性能优化过程。在优化时,作者重写了BaseMapper的`selectPage`方法,通过`Wrapper`的`between`操作添加了ID区间限制以提升SQL执行效率。然而,由于在业务服务类中循环调用分页查询时未每次都创建新的`Wrapper`对象,导致`id BETWEEN ? AND ?`条件重复出现在SQL中。那么,如何解决这个问题呢?

背景源于一次性能优化

我们的springboot应用程序,持久层用的是mybatisplus-3.5.1。

 

在一次对交易数据的分页查询代码做性能优化时,我在交易Mapper类-SbhPlatOrderMapper里重写了父接口BaseMapper的selectPage。其中,调用Wrapper<T>参数对象的between操作,为最终的sql加上了id区间限制,以提高sql执行性能。

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface SbhPlatOrderMapper extends BaseMapper<SbhPlatOrder> {    

    @Override
    default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper){
        PrePageDto prePageDto = selectCountCache(queryWrapper);
        page.setTotal(prePageDto.getRowCount());
        if (prePageDto.getRowCount()>0) {
            if (queryWrapper instanceof LambdaQueryWrapper) {
                ((LambdaQueryWrapper<SbhPlatOrder>) queryWrapper).between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
            } else if (queryWrapper instanceof QueryWrapper) {
                ((QueryWrapper<SbhPlatOrder>) queryWrapper).lambda().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
            }
            page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), queryWrapper));
        }
        return page;
    }
    
    @Cacheable(cacheNames = RedisConfig.SBH_PLAT_ORDER_COUNT_CACHE_KEY,
            key = "T(com.emax.zhenghe.common.util.security.MD5Util).md5Encode(#queryWrapper.customSqlSegment)")
    PrePageDto selectCountCache(@Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper);

    List<SbhPlatOrder> selectPageList(long offset, long pageSize, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper);
}

SbhPlatOrderMapper.xml:

    <select id="selectCountCache" resultType="com.emax.etl.provider.modules.dto.PrePageDto">
        select count(1) as rowCount, max(id) as maxId, min(id) as minId from sbh_plat_order ${ew.customSqlSegment}
    </select>
    <select id="selectPageList" resultMap="sbhPlatOrderLimitResultMap">
        select
        <include refid="Base_Column_List"/>
        from
        sbh_plat_order
        ${ew.customSqlSegment}
        limit #{offset}, #{pageSize}
    </select>

 

问题-小瑕疵来了

程序上线后,性能的确有改观。不过呢,发现了一个小瑕疵。见下面日志截图。where条件里的“id BETWEEN ? AND ?”重复了N遍。

 

原来呢,在业务service类中在使用while循环调用这个selectPage时,并不是每次都new一个Wrapper参数,而是重复使用同一个Wrapper对象。见下图。 这样一来,就不难理解 where条件里重复多次 “id BETWEEN ? AND ?” 了。

最直接的解决办法 -Wrapper#getCustomSqlSegment

头疼医头的方式,是修改调用的代码,让service内每次在调用分页时都new一个Wrapper对象参数。

显然,这样解决问题只是一时。以后再有循环调用这个分页的地方,免不了可能还存在这个瑕疵。以我的风格,我通常不会以这样的方式来解决问题。

我的解决办法是利用 Wrapper#getCustomSqlSegment, 上面sql里重复出现的是 “id BETWEEN”。所以,修正的代码如下:

    // 外面调用处可能复用这个queryWrapper对象。所以,为避免重复追加条件,这里先做判读再追加。
    if (!queryWrapper.getCustomSqlSegment().contains("id BETWEEN")) {
        if (queryWrapper instanceof LambdaQueryWrapper) {
            ((LambdaQueryWrapper<SbhPlatOrder>) queryWrapper).between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        } else if (queryWrapper instanceof QueryWrapper) {
            ((QueryWrapper<SbhPlatOrder>) queryWrapper).lambda().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        }
    }

 

更好的解决办法-对象克隆

上面的解决办法存在两点不足:①使用了字符串contains操作,不利于维护,如果日后id字段重命名,而忽略了这里的修改,就出现bug;②限定了Wrapper只调用一次between操作,而在某些情况下maxId/minId可能是会发生变化的,这样就又会出现隐患。mybatisplus的Wrapper也不提供移除某个已经存在的条件的操作api。

我注意到,QueryWrapper和LambdaQueryWrapper的clone方法。经测试,可行。

下面源码,是mybatisplus的抽象类AbstractWrapper,重写的超类Object#clone。

package com.baomidou.mybatisplus.core.conditions;

public abstract class AbstractWrapper ...{

    @Override
    @SuppressWarnings("all")
    public Children clone() {
        return SerializationUtils.clone(typedThis);
    }

}

如此一来,上面的selectPage方法就可以进一步改造一番了。写这篇博文是子夜11:20,我即将关机休息。三下五除二,我就在这篇blog的文本编辑器里敲击键盘,写出下面的selectPage代码:

    @Override
    default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper){
        Wrapper<SbhPlatOrder> queryWrapperClone = queryWrapper.clone();
        // 下面的代码同上,只不过都访问的是queryWrapperClone ,而不再是queryWrapper。
        。。。
    }

第二天上班后,我开始沿着上面的思路改工程里的代码。却发现行不通!

经过梳理才发现,原来mybatisplus中,各种Wrapper的关系是下面这样子滴。

这个类关系图传递如下信息:

  • QueryWrapper与LambdaQueryWrapper 两者本身不存在继承关系,两者都继承自AbstractWrapper。
  • QueryWrapper和LambdaQueryWrapper 与 他们的抽象父类AbstractWrapper 的泛型不同,AbstractWrapper类有3个泛型参数。 所以,试图将上面selectPage方法的第二个参数类型由Wrapper改为子类AbstractWrapper,然后再在方法第一行调用其clone方法是不可以的。

 

结合起来,将上面selectPage方法代码做如下改动:

@Override
default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper) {
    PrePageDto prePageDto = selectCountCacheEnhance(queryWrapper);
    page.setTotal(prePageDto.getRowCount());
    if (prePageDto.getRowCount() > 0) {
        // 外面调用处可能复用这个queryWrapper对象。所以,为避免重复调用between等操作追加条件,做如下处理
        if (queryWrapper instanceof LambdaQueryWrapper) {
            LambdaQueryWrapper<SbhPlatOrder> clonedWrapper = ((LambdaQueryWrapper<SbhPlatOrder>) queryWrapper).clone();
            clonedWrapper.between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
            page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), clonedWrapper));
        } else if (queryWrapper instanceof QueryWrapper) {
            QueryWrapper<SbhPlatOrder> clonedWrapper = ((QueryWrapper<SbhPlatOrder>) queryWrapper).clone();
            clonedWrapper.lambda().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
            page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), clonedWrapper));
        }
    }
    return page;
}

代码改成下面这样子,能更直观体现出来QueryWrapper、LambdaQueryWrapper、AbstractWrapper这三者之间的关系。不过,因为AbstractWrapper是泛型类,这里IDE会提示:Raw use of parameterized class 'AbstractWrapper' 。

@Override
default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper) {
    PrePageDto prePageDto = selectCountCacheEnhance(queryWrapper);
    page.setTotal(prePageDto.getRowCount());
    if (prePageDto.getRowCount() > 0) {
        // 外面调用处可能复用queryWrapper参数。所以,为避免重复调用between等操作追加条件,做如下处理
        AbstractWrapper clonedWrapper = null;
        if (queryWrapper instanceof LambdaQueryWrapper) {
            clonedWrapper = ((LambdaQueryWrapper<SbhPlatOrder>) queryWrapper).clone().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        } else if (queryWrapper instanceof QueryWrapper) {
            clonedWrapper = ((QueryWrapper<SbhPlatOrder>) queryWrapper).clone().lambda().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        }
        page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), clonedWrapper));
    }
    return page;
}




 

EOF-thanks for reading.

目录
相关文章
|
6月前
|
Java 数据库连接 数据库
Mybatis-plus中的QueryWrapper的多种用法!(总结)
Mybatis-plus中的QueryWrapper的多种用法!(总结)
708 0
|
4月前
|
SQL
条件构造器,MybatisPlus支持各种复杂的where条件,其实就是Wrapper,eq是等于的意思,相当于等于那个数值,ne就是不等于,gt大于的意思,ge大于等于,QueryWrapper是做
条件构造器,MybatisPlus支持各种复杂的where条件,其实就是Wrapper,eq是等于的意思,相当于等于那个数值,ne就是不等于,gt大于的意思,ge大于等于,QueryWrapper是做
|
9天前
|
SQL Java 数据库连接
mybatisplus QueryWrapper or写法
# MyBatis-Plus QueryWrapper的OR写法详解 MyBatis-Plus是一款基于MyBatis的增强工具,提供了丰富的简化操作,使开发者能更高效地进行数据库操作。`QueryWrapper`是MyBatis-Plus中用于构建查询条件的一个核心类,支持多种条件组合,包括AND和OR条件。本文将详细介绍如何使用 `QueryWrapper`实现OR条件的查询。 ## QueryWrapper简介 `QueryWrapper`用于构建动态SQL查询条件,它封装了各种条件构造方法,使得查询条件的构建更加简洁和直观。`QueryWrapper`中提供了丰富的方法来支持多
15 0
|
6月前
|
SQL 缓存 安全
深入解析MyBatis-Plus LambdaQueryWrapper与QueryWrapper:高效数据查询的秘密
深入解析MyBatis-Plus LambdaQueryWrapper与QueryWrapper:高效数据查询的秘密
7798 2
|
6月前
|
数据库
MyBatis-Plus中的QueryWrapper
MyBatis-Plus中的QueryWrapper
175 0
|
Java 数据库连接 数据库
Mybatis Plus 中的LambdaQueryWrapper简介
Mybatis Plus 中的LambdaQueryWrapper简介
1101 0
|
Java 数据库连接 mybatis
mybatis plus常用的QueryWrapper条件参数
mybatis plus常用的QueryWrapper条件参数
153 0
mybatis plus常用的QueryWrapper条件参数
MybatisPlus 中QueryWrapper 方法介绍
MybatisPlus 中QueryWrapper 方法介绍
145 0
MybatisPlus 中QueryWrapper 方法介绍
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
105 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
52 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块