草庐IT

没用好mybatisplus的Wrapper,我真尴尬啊

buguge - Keep it simple,stupid 2023-04-16 原文

QueryWrapper/LambdaQueryWrapper/AbstractWrapper/Wrapper... 一图看懂mybatisplus中各个Wrapper类的关系图

背景

我们的springboot应用程序的持久层,是用jeecgboot框架生成的代码。其中,mybatisplus版本是3.1.2。

 

在一次对交易数据的分页查询代码做性能优化时,我在Mapper里重写了父接口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);
}

 

问题描述

程序上线后运行了一段时间。后来,发现了一个问题。见下面日志截图

 

原来呢,在业务service类中调用这个分页的方法里,并不是每次调用分页时都new一个Wrapper对象,而是重复使用同一个Wrapper对象。见下图。 所以,出现上图的问题就不难理解了。

  

解决办法 -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;②限定了只调用一次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,我即将关机休息。三下五除二,我就在这篇文章的文本编辑器里写下面的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方法代码做如下改动:

if (prePageDto.getRowCount() > 0) {
    // 外面调用处可能复用这个queryWrapper对象。所以,为避免重复调用between等操作追加条件,做如下处理
    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.

有关没用好mybatisplus的Wrapper,我真尴尬啊的更多相关文章

  1. Ruby: else 没有 rescue 是没用的 - 2

    我是ruby新手。我正在尝试编写一个apacheerror.log监视器。大部分已经完成,但我收到警告:否则没有救援是无用的。我不知道我做错了什么。Ruby是否希望我使用“unless”?classErrorMonitor@@previous_size=0@@counter=0definitialize()enddefprocessif@@counter>0@new_size=File.stat('/var/log/apache2/error.log').sizeif@new_size>@@previous_sizeforiin@@previous_size..@new_size-@@

  2. ruby -/usr/bin/env ruby​​_noexec_wrapper 失败,没有文件或目录 - 2

    当我尝试将chef-solr作为服务启动时,它因以下错误而失败#servicechef-solrstartStartingchef-solr:/usr/bin/env:ruby_noexec_wrapper:Nosuchfileordirectory[FAILED]但是当我从命令行手动运行它时它运行成功#chef-solr-d-c/etc/chef/solr.rb-L/var/log/chef/solr.log-P/var/run/chef/solr.pid#echo$?0#ps-ef|grepchefroot269111204:19?00:00:01java-Xmx256M-Xms2

  3. MyBatisPlus总结 - 2

    目录MyBatisPlusMP特点MP框架结构MP使用准备导入依赖springboot整合mybatisplus配置文件定义好实体类User后编辑mapper接口@Mapper与@MapperScan("包名")区别MP基本操作新增操作删除操作通过id删除用户通过map作为条件删除通过多个id实现删除更新用户通过id进行用户更新查询用户 根据id查询用户根据多个id查询用户根据map集合作为条件查询用户通用Service接口一些操作 查询总记录数批量添加数据MP常用注解雪花算法前言垂直分表水平分表条件构造器继承结构使用条件构造器实现查询操作查询所有用户根据构造器查询主键字段集合根据条件构造器查

  4. javascript - 为什么简化的 CommonJS Wrapper 语法在我的 Dojo AMD 模块上不起作用? - 2

    我开始思考requirejs和新的DojoAMD结构,但我在一些早期测试中遇到了问题:cg/signup.js:define(['dojo/_base/fx','dojo/dom'],function(fx,dom){returnfunction(){this.hidePreloader=function(id){varpreloader=dom.byId(id);fx.fadeOut({node:preloader}).play()}}})这很好用。在主cg.js文件中:require(['dojo/_base/kernel','dojo/_base/loader'])dojo.re

  5. javascript - NodeJS & Gulp Streams & Vinyl File Objects - Gulp Wrapper for NPM package producing incorrect output - 2

    目标我目前正在尝试为NPMFlat编写一个Gulp包装器可以很容易地在Gulp任务中使用。我觉得这对Node社区很有用,也可以实现我的目标。Therepositoryishereforeveryonetoview,contributeto,playwithandpullrequest.我正在尝试制作多个JSON文件的扁平化(使用点表示法)副本。然后我想将它们复制到同一个文件夹并修改文件扩展名以从*.json更改为*.flat.json。我的问题我在JSON文件中返回的结果看起来像乙烯基文件或字节码。例如,我希望输出像"views.login.usernamepassword.login

  6. 一些没用的AD技巧——AD PCB直角走线处理与T型滴泪。 - 2

    ADPCB直角走线处理与T型滴泪。在PCB布板过程中为了避免走线出现直角,一般会对此类走线进行处理。1.使用手动走线的方式进行处理。首先将参数设定中PCBEditor-InteractiveRouting中的自动移除闭合回路选项勾掉,否则会出现下图的情况单击直角相邻两根线上最近的两处栅格点进行走线,就可以进行直角处理。另一侧也按同样方法进行。即可完成直角布线的处理。2.使用AD中滴泪功能进行处理。选择工具中的滴泪功能。选择WorkingMode选择ADD,Objects选择ALL,点击OK。注:添加滴泪时需要确保直角走线连接情况,如下图所示。即可完成直角布线的处理。直角走线连接不当的情况下会出

  7. javascript - react -JS : Access a child's method through HOC wrapper - 2

    我有一个看起来像这样的Editor组件:classEditorCompextendsComponent{focus(){this.refs.input.focus();}render(){return();}}这样使用EditorComp的元素可以设置一个ref并调用它的focus方法并将焦点应用到较低级别的输入,如下所示:classParentextendsComponent{render(){return(this.refs.editor.focus()}>Focus);}}但是,当将EditorComp包装在高阶组件(如react-redux的connect())中时,Edito

  8. javascript - 这个(没用的?)javascript 代码有什么作用? - 2

    在调试使用jQuery的javascript代码时,我发现了以下代码:[0,0].sort(function(){baseHasDuplicate=false;return0;});根据我对javascript的理解,这段代码将使用比较函数对包含两个零的数组进行排序,比较函数将始终设置一个全局变量并返回相等性,这与baseHasDuplicate=false;具有相同的效果。来自一个有值(value)的来源,我想我错过了一些东西。我是不是错过了什么或者这是编程失败? 最佳答案 如你所见here(中文),此代码可能用于测试Chrome

  9. javascript - 拖放,防止尴尬的突出显示? - 2

    我正在构建一个拖放方法,使用查询-onmousedown导致-onmousemove(拖动)然后-onmouseup(取消绑定(bind)onmousemove)问题是,浏览器默认开始突出显示onmousemove,它会飞过整个页面并取消事件,使其无法使用。知道如何防止突出显示,因为preventDefault似乎不起作用。这是我的代码(是的,它非常草率,抱歉!)$(".middleRow").mousedown(function(){calculateSelection();$('body').append(''+numSelected+'messages');$(document)

  10. go - Go 中令人尴尬的并行任务的惯用解决方案是什么? - 2

    我目前正在关注以下代码的增强版本:funcembarrassing(data[]string)[]string{resultChan:=make(chanstring)varwaitGroupsync.WaitGroupfor_,item:=rangedata{waitGroup.Add(1)gofunc(itemstring){deferwaitGroup.Done()resultChan这让我大吃一惊。所有这一切都可以用其他语言表达为results=parallelMap(data,doWork)即使在Go中不能这么容易地完成,难道没有比上述更好的方法吗?

随机推荐