教你如何写出高性能的Mybatis分页插件

原创 吴就业 88 0 2020-03-21

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/deecdcd02d5e4057ae315cd4b5844dfa

作者:吴就业
链接:https://wujiuye.com/article/deecdcd02d5e4057ae315cd4b5844dfa
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

最近做的一个需求需要写复杂的SQL,且需要分页,我是非常懒的人,因为项目中使用了mybatis-plus,因此分页想着使用mybatis-plus的分页插件自动完成。但是测试时发现分页性能下降,sql中的子查询并没有去掉,只是在原有sql的基础上包装了一层select count(*)

我在前面一篇介绍mybatis-plus的文章中,就分析了它提供的分页插件的源码,并非常肯定的推荐大家去使用这个分页插件。因为它会优化sql,优化后的查询总数的sql并不比自己写的查询总数的sql性能差。《[使用Mybatis-Plus提高开发效率]()》

但今天我调试发现并非如此,很是吃惊,然后我就想着不行就自己优化sql呗。mybatis-plus的分页插件PaginationInterceptor,支持自己写sql优化器,可通过自定义sql优化器提供分页插件的性能。

 PaginationInterceptor interceptor = new PaginationInterceptor();
 interceptor.setSqlParser(自定义的sql优化器);

在创建分页插件时,如果不传sql优化器,则会使用mybatis-plus提供的默认优化器JsqlParserCountOptimize。而它提供的默认优化器正是使用JsqlParser这个开源的sql解析工具包实现的。

本篇其实也重点强调理解框架源码的重要性,只有对源码有足够的了解,遇到问题才能迎刃而解。本篇介绍是什么原因导致的mybatis-plus分页插件性能下降,以及如何通过使用JsqlParser这个开源的sql解析工具包与mybatis-plus提供的自定义sql优化器功能,自己实现高性能的分页插件。

其实也在是在自己实现优化器的过程中,才发现在SQL解析失败的情况下,分页插件不会优化SQL,而是直接在原sql基础上直接包装一层select count(*),导致性能下降。

如果不是因为刚接触mybatis-plus时,好奇去看了下它提供的分页插件的源码,今天估计就是自己实现分页查询了。

public SqlInfo optimizeSql(MetaObject metaObject, String sql) {
        SqlInfo sqlInfo = SqlInfo.newInstance();
        try {
            // 通过优化器优化原sql
            .......
        } catch (Throwable var11) {
            // SqlUtils.getOriginalCountSql(sql) 这句是给sql包装一层查询总数
            sqlInfo.setSql(SqlUtils.getOriginalCountSql(sql));
            return sqlInfo;
        }
    }

这是JsqlParserCountOptimize的源码。在解析sql出错时,不会报错,而是直接在原SQL基础上直接包装一层select count(*)

出现使用JsqlParser解析sql失败的情况,就需要去检查自己写的sql是否有问题,首先是排除sql中字符串是否使用了双引号。如

select ifnull(NAME,"") as name from user

再检查sql是否使用了数据库提供的特殊函数,这种情况下JsqlParser也会解析失败,如下面这句sql,可能是因为使用了IF函数,导致JsqlParser解析sql失败。

  concat(ifnull(a.NAME,''),IF(a.NAME is null,'','>'),
        ifnull(b.NAME,''),IF(b.NAME is null,'','>'),
        ifnull(c.NAME,'')) as name

JsqlParser解析sql失败时,会在异常中提示sql哪个地方解析出错,所以很容易找到原因。在找到原因后,我优化了下sql

concat(
        (case when a.`NAME` is null then '' else concat(a.`NAME`,'>') end),
        (case when b.`NAME` is null then '' else concat(b.`NAME`,'>') end),
        (case when c.`NAME` is null then '' else c.`NAME` end)
   ) as name

修改之后mybatis-plus的分页插件便能正常自动帮优化sql,也就不需要自己写优化器。

下面是教大家如果自己去实现一个简单的优化器,自己优化查询总数的sql。就是去掉sql中的子查询。虽然写出来了,但我并没有使用,既然问题已经解决,就不使用了,怕会导致项目中的某些分页查询异常。虽然用不上,但学习是快乐的,说不到以后会用到这个知识点。

    @Bean
    @Order(10)
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor interceptor = new PaginationInterceptor();
        interceptor.setSqlParser((metaObject, s) -> {
            try {
                Statement jSqlParser = CCJSqlParserUtil.parse(s);
                jSqlParser.accept(new StatementVisitorAdapter() {
                    @Override
                    public void visit(Select select) {
                        select.getSelectBody().accept(new SelectVisitorAdapter() {
                            @Override
                            public void visit(PlainSelect plainSelect) {
                                if (!CollectionUtils.isEmpty(plainSelect.getSelectItems())) {
                                    // 遍历select item
                                    // 如:a.ID, a.Name, ....
                                    // 去掉嵌套子查询
                                    for (Iterator<SelectItem> iterator = plainSelect.getSelectItems().iterator();
                                         iterator.hasNext(); ) {
                                        SelectItem item = iterator.next();
                                        boolean[] flag = new boolean[]{false};
                                        // 判断是否存在子查询
                                        item.accept(new SelectItemVisitorAdapter() {
                                            @Override
                                            public void visit(SelectExpressionItem item) {
                                                item.getExpression().accept(new ExpressionVisitorAdapter() {
                                                    @Override
                                                    public void visit(SubSelect subSelect) {
                                                        flag[0] = true;
                                                    }
                                                });
                                            }
                                        });
                                        // 移除嵌套子查询
                                        if (flag[0]) {
                                            iterator.remove();
                                        }
                                    }
                                }
                            }
                        });
                    }
                });
                SqlInfo sqlInfo = SqlInfo.newInstance();
                sqlInfo.setSql(SqlUtils.getOriginalCountSql(jSqlParser.toString()));
                return sqlInfo;
            } catch (JSQLParserException e) {
                SqlInfo sqlInfo = SqlInfo.newInstance();
                sqlInfo.setSql(SqlUtils.getOriginalCountSql(s));
                return sqlInfo;
            }
        });
        return interceptor;
    }

在解析sql异常时,不能抛出异常,而是跳过优化,直接使用原sql。毕竟业务功能第一,不能影响系统的正常运行,这也是mybatis-plus的分页插件性能会下降的原因。

jSqlParser这个工具包使用了访问者模式让我们去修改sqlCCJSqlParserUtil.parse(s)解析sql,之后就可以通过accept去访问sql的每个部分,因为我想去掉sqlselect部分嵌套的子查询,因此第一步就是访问select部分。

jSqlParser.accept(new StatementVisitorAdapter() {
            @Override
            public void visit(Select select) {
        });

拿到select部分之后,可以继续accept去遍历每一个选项,查看是否存在子查询情况,如果存在则将这个选项移除掉。如

select a.id,
(select b.`NAME` from b where b.`ID`=a.`B_ID`) as name
from a

去掉子查询后就是

select id from a

拿优化后的sql再包装一层select count(*)就能自己实现一个简单的高性能分页插件。

比如:

select count(*) from (
        select a.`ID`,a.`NAME`,
        (select b.`NAME` from b where b.`ID`=a.`B_ID`) as bname
        from a
        where .....
) as total;

使用自己写的优化器优化后的查询总数的sql

select count(*) from (
        select a.`ID`,a.`NAME`
        from a
        where .....
) as total;

而使用mybatis-plus提供的优化器优化后的查询总数的sql

select count(*) from (
        select a.`ID`
        from a
        where .....
) as total;

关于jSqlParser这个工具包,实在不懂怎么去介绍,感兴趣可以自己去试错,去摸索。先从StatementVisitor这个访问器入手,在每个visit方式中下个断点,看下每个visit方法传递的参数都是sql的哪个部分,比如select部分,再继续看SelectVisitor这个访问器的所有visit方法…。这种方法虽然有点蠢,不过好过看英文的API文档。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

教你如何将开源项目发布到maven中央仓库

如何将开源项目发布到maven中央仓库,让别人通过依赖使用你的开源项目,想必很多朋友都有过这个想法。

如何优化大表分页查询的Limit性能问题?

如果表的数据量非常大,我们除了优化查询总数的`sql`之外,还是需要优化`limit`的。

Redis实现原子操作的两种方式与商品入库出库解决方案

想要实现针对多个key的复杂原子操作有两种方法。一种是Watch+Multi,即监视器加事务方式,另一种便是通过执行lua脚本实现。

一道很有意思的Redis面试题,关于Bitmap算法,我选出了一些优质评论

起源于我在一个短视频中分享的一道面试题,当然,这道面试题我确实在工作中用过,只是业务场景不同。

一篇文章说清楚Java的全局异常处理,深入到hotspot源码

本篇将介绍如何使用Java提供的全局异常处理,以及分析一点hotspot虚拟机的源码,让大家了解虚拟机是如何将异常交给全局异常处理器处理的。

使用Docker部署用于学习的ElasticSearch集群

在Linux服务器上使用 Docker安装ElasticSearch集群。