I’m so embarrassed that I didn’t use mybatisplus’ Wrapper well.

QueryWrapper/LambdaQueryWrapper/AbstractWrapper/Wrapper… One picture to understand the relationship diagram of each Wrapper class in mybatisplus

Background

The persistence layer of our springboot application is code generated using the jeecgboot framework. Among them, the mybatisplus version is 3.1.2.

When optimizing the performance of the paging query code for transaction data, I rewrote the selectPage method of the parent interface BaseMapper in Mapper. Among them, the between operation of the Wrapper parameter object is called to add ID interval restrictions to the final SQL to improve SQL execution performance.

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);
}

Problem description

The program ran for a while after it went online. Later, a problem was discovered. See the log screenshot below

It turns out that in the method of calling this paging in the business service class, a new Wrapper object is not created every time paging is called, but the same Wrapper object is reused. See below. Therefore, it is not difficult to understand the problem shown above.

Solution -Wrapper#getCustomSqlSegment

The way to solve the headache is to modify the method of calling this paging in the service class, and create a new Wrapper object before each paging call.

Obviously, this solution to the problem is only temporary. This problem will still exist if this paging is called in the future.

Therefore, my solution is to use Wrapper#getCustomSqlSegment. The repeated occurrence in the above sql is “id BETWEEN”. So, the corrected code is as follows:

 // External calling sites may reuse this queryWrapper object. Therefore, in order to avoid repeatedly adding conditions, the interpretation is done here first and then added.
    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());
        }
    }

A better solution-object cloning

There are two flaws in the above solution: ① The string contains operation is used, which is not conducive to maintenance. If the id field is renamed in the future and the modification here is ignored, bugs will occur; ② The between operation is limited to being called only once, and in In some cases, maxId/minId may change, which may cause hidden dangers. Mybatisplus’s Wrapper does not provide an operation API for removing an existing condition.

I noticed that the clone method of QueryWrapper and LambdaQueryWrapper. After testing, it works.

The source code below is the abstract class AbstractWrapper of mybatisplus and the rewritten super class Object#clone.

package com.baomidou.mybatisplus.core.conditions;<br>
public abstract class AbstractWrapper ...{

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

}

In this way, the selectPage method above can be further modified. It’s 11:20 midnight as I write this blog post, and I’m about to shut down my phone and take a rest. Three times five times two, I will write the following selectPage code in the text editor of this article:

 @Override
    default IPage<SbhPlatOrder> selectPage(IPage<SbhPlatOrder> page, @Param(Constants.WRAPPER) Wrapper<SbhPlatOrder> queryWrapper){
        Wrapper<SbhPlatOrder> queryWrapperClone = queryWrapper.clone();
        //The following code is the same as above, except that they access queryWrapperClone instead of queryWrapper.
        . . .
    }

After going to work the next day, I started to change the code in the project along the above ideas. But found it didn’t work!

After sorting it out, I discovered that the relationship between various wrappers in mybatisplus is as follows:

This class diagram conveys the following information:

  • There is no inheritance relationship between QueryWrapper and LambdaQueryWrapper. Instead, they all inherit AbstractWrapper.
  • QueryWrapper and LambdaQueryWrapper are different from the generics of their abstract parent class, AbstractWrapper, which has 3 generic parameters. Therefore, it is not possible to try to change the second parameter type of the above selectPage method from Wrapper to the subclass AbstractWrapper, and then call its clone method in the first line of the method.

Combined, make the following changes to the selectPage method code above:

if (prePageDto.getRowCount() > 0) {
    // External calling sites may reuse this queryWrapper object. Therefore, in order to avoid repeatedly calling between and other operations to add conditions, do the following processing
    if (queryWrapper instanceof LambdaQueryWrapper) {
        LambdaQueryWrapper<SbhPlatOrder> clone = ((LambdaQueryWrapper<SbhPlatOrder>) queryWrapper).clone();
        clone.between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), clone));
    } else if (queryWrapper instanceof QueryWrapper) {
        QueryWrapper<SbhPlatOrder> clone = ((QueryWrapper<SbhPlatOrder>) queryWrapper).clone();
        clone.lambda().between(SbhPlatOrder::getId, prePageDto.getMinId(), prePageDto.getMaxId());
        page.setRecords(selectPageList((page.getCurrent() - 1) * page.getSize(), page.getSize(), clone));
    }
}

EOF-thanks for reading.