背景源于一次性能优化
我们的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> { default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, (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; } cacheNames = RedisConfig.SBH_PLAT_ORDER_COUNT_CACHE_KEY, ( key = "T(com.emax.zhenghe.common.util.security.MD5Util).md5Encode(#queryWrapper.customSqlSegment)") PrePageDto selectCountCache( (Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper); List<SbhPlatOrder> selectPageList(long offset, long pageSize, (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 ...{ "all") ( public Children clone() { return SerializationUtils.clone(typedThis); } }
如此一来,上面的selectPage方法就可以进一步改造一番了。写这篇博文是子夜11:20,我即将关机休息。三下五除二,我就在这篇blog的文本编辑器里敲击键盘,写出下面的selectPage代码:
default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, (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方法代码做如下改动:
default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, (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' 。
default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, (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.