因为最近工作切实太忙,肝了一段时间后,终于有空来写点文章了。闲话少说,进入本期内容的正题。
省流版本:PagerHelper在与PostgreSQL搭配应用时,会有坑!
你如果想晓得具体是什么样的一个坑,往下看,我来给大伙儿开展说说。

背景
最近在做一个我的项目,惯例的SpringBoot+Mybatis架构,数据库应用的是PostgreSQL,并且有一些场景须要应用到分页查问,很天然的就应用了PageHelper这个分页工具,如果你是初学者或者还没有应用过PageHelper,能够理解一下。PageHelper
到这里所有都很顺利,然而聪慧如你肯定猜到了,要有什么事件产生。
我须要实现一个数据权限的性能,依据每个用户的数据权限不同,管制每个用户只能查问到本人有权限的用户。
比方用户A只能拜访他所在组织的数据,用户B是一个高级用户,能查问到多个组织的数据,用户C是超级管理员,能查问到所有数据等。
实现这个性能我采纳的形式是在进行查问时,依照用户的权限不同,减少对应的查问条件。
比方业务办法中SQL是这样的:
select * from t_data where data_id = 1 limit 10 offset 0
复制代码
我须要批改成:
select * from t_data where data_id = 1 and data_part in (1,2,3) limit 10 offset 0
复制代码
这里须要强调一点,就是我不仅要对t_data这一张表减少条件,还要对其余很多张表加条件,并且可能每张表加的条件还不一样。
我采纳了MyBatis的拦截器性能,在拦截器中对立对SQL进行解决,增加数据权限条件。
原以为所有会和我的预期一样,没想到中途踩了一个坑,让我折腾了半早晨。
以上是踩坑的背景,接下来看一下代码。
代码实现
我在拦截器中是这样做的。
MyBatis拦截器实现

/**

  • 数据权限拦截器
  • 拦挡所有MyBatis的查询方法
  • @author 小黑说
  • @version 1.0
    */

@Intercepts({

@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})

public class DataPermissionInterceptor implements Interceptor {

private static final Logger log = LoggerFactory.getLogger(DataPermissionInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {    // 这是一个提前解决好的以后用户数据权限的汇合,寄存在ThreadLocal中。    List<Condition> conditions = DataConditionHelper.getConditions();    // 如果conditions为空代表该用户没有数据权限的限度,能够查问所有数据。    if (CollectionUtils.isEmpty(conditions)) {        return invocation.proceed();    }    Object[] args = invocation.getArgs();    MappedStatement ms = (MappedStatement) args[0];    Object parameter = args[1];    RowBounds rowBounds = (RowBounds) args[2];    ResultHandler resultHandler = (ResultHandler) args[3];    Executor executor = (Executor) invocation.getTarget();    CacheKey cacheKey;    BoundSql boundSql;    if (args.length == 4) {        boundSql = ms.getBoundSql(parameter);        cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);    } else {        cacheKey = (CacheKey) args[4];        boundSql = (BoundSql) args[5];    }    // 对以后查问语句进行数据权限加强解决    String newSql = handleSql(boundSql.getSql());    // 从新new一个查问语句对象    BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());    // 把新的查问SQL放到statement里    MappedStatement newMs = newMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));    for (ParameterMapping mapping : boundSql.getParameterMappings()) {        String prop = mapping.getProperty();        if (boundSql.hasAdditionalParameter(prop)) {            newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));        }    }    return executor.query(newMs, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);}/*** 加强SQL*/private String handleSql(String origSql) {    SQLExpr sqlExpr = SQLUtils.toSQLExpr(origSql, JdbcConstants.POSTGRESQL);    DataPermissionSelectVisitor selectVisitor = new DataPermissionSelectVisitor();    sqlExpr.accept(selectVisitor);    return sqlExpr.toString();}@Overridepublic Object plugin(Object target) {    if (target instanceof Executor) {        return Plugin.wrap(target, this);    }    return target;}

}

复制代码
SQL加强性能
在对SQL进行加强时,我须要将本来的SQL语句解析成形象语法树(AST),你能够了解为将SQL语句转换成一个java对象的树形构造,用Java对象示意SQL中的所有元素,如查问字段,WHERE,JOIN等,而后我再对须要增加条件的表减少数据权限条件。
解析SQL语句曾经有很多成熟的工具,不必本人开发,我这里应用的阿里的druid包中的SQLUtils。
重点介绍我减少条件的代码。
public class DataPermissionSelectVisitor extends PGASTVisitorAdapter {

private static final String TABLE_IGNORE_CHAR = "\"|'|`";@Overridepublic boolean visit(SQLExprTableSource x) {    // 数据权限条件    List<Condition> dataConditions = DataConditionHelper.getConditions();    if (CollectionUtils.isEmpty(dataConditions)) {        return super.visit(x);    }        SQLName tableName = x.getName();    String alias = x.getAlias();    // 寄存能够增加的条件sql片段    List<String> conditionSqls = new ArrayList<>();    for (Condition condition : dataConditions) {        String name = tableName.getSimpleName().replaceAll(TABLE_IGNORE_CHAR, "");        if (name.equals(condition.getTableName())) {            // 数据权限中的表名和以后SQL中的表名统一,则须要增加数据权限条件            if (!StringUtils.isEmpty(alias)) {                conditionSqls.add(alias.concat(".").concat(condition.buildCondition()));            } else {                conditionSqls.add(condition.buildCondition());            }        }    }    if (CollectionUtils.isEmpty(conditionSqls)) {        return super.visit(x);    }    SQLObject parent = x.getParent();    // 单表查问    if (parent instanceof SQLSelectQueryBlock) {        SQLSelectQueryBlock query = (SQLSelectQueryBlock) parent;        // 创立条件对象        SQLExpr expr = buildExpr(conditionSqls);        // 将条件对象增加到query对象中        query.addCondition(expr);    }    // 联表查问    if (parent instanceof SQLJoinTableSource) {        SQLJoinTableSource query = (SQLJoinTableSource) parent;        SQLExpr expr = buildExpr(conditionSqls);        query.addConditionn(expr);    }    return super.visit(x);}

/**

* 构建条件对象*/private SQLExpr buildExpr(List<String> conditionSqls) {    SQLExpr expr = null;    for (String conditionSql : conditionSqls) {        if (expr == null) {            expr = SQLUtils.toSQLExpr(conditionSql, JdbcConstants.POSTGRESQL);        } else {            SQLExpr sqlExpr = SQLUtils.toSQLExpr(conditionSql, JdbcConstants.POSTGRESQL);            expr = SQLUtils.buildCondition(SQLBinaryOperator.BooleanOr, expr, true, sqlExpr);        }    }    return expr;}

}
复制代码
只须要在拦截器中依照上面办法中应用即可。
private String handleSql(String origSql) {

// 将SQL解析成SQLExpr对象SQLExpr sqlExpr = SQLUtils.toSQLExpr(origSql, JdbcConstants.POSTGRESQL);// 应用Visitor对象的accpet办法,减少数据权限条件DataPermissionSelectVisitor selectVisitor = new DataPermissionSelectVisitor();sqlExpr.accept(selectVisitor);return sqlExpr.toString();

}
复制代码
到这里,如同所有都很顺利,我测试了几个一些单表查问和联表查问都没有问题。
直到我测了一下一个带分页性能的查问,第一页没有问题,失常返回。

然而,从第二页开始,出问题了。

异样信息中我标注出了几个要害信息。

从这个异样信息根本能够猜到,应该是SQL语句中的参数占位符和传入的参数数量不匹配导致的。
个别遇到这种问题,我第一反馈是先面向网络编程一把。

很显然,网友曾经提前踩过相似的坑了。一圈找下来,根本能够必定和我猜的统一。
接下来就debug看看吧,初步思路是看看在应用分页查问时,减少数据权限前后的SQL语句的变动。
debug走起
对SQL进行批改的办法:
private String handleSql(String origSql) {

// 减少条件之前SQLExpr sqlExpr = SQLUtils.toSQLExpr(origSql, JdbcConstants.POSTGRESQL);DataPermissionSelectVisitor selectVisitor = new DataPermissionSelectVisitor();sqlExpr.accept(selectVisitor);// 减少条件之后return sqlExpr.toString();

}
复制代码
那咱们来debug一下看看,在减少数据权限条件之前,分页查问语句是上面这样的:

在减少数据权限之后,分页查问语句变更了上面这样:

什么鬼啊,怎么就剩limit了?!offset被吃了?!

沉着一下,这个分页语句有点不对劲。
这里须要阐明一下,postgreSQL的分页语句写法和MySQL的有点区别,在MySQL中分页语句的写法是:
-- MySQL分页语法
SELECT * from tableName where 1=1 limit 10 offset 10;
SELECT * from tableName where 1=1 limit 0 , 10;
SELECT * from tableName where 1=1 limit 10;
复制代码
然而postgreSQL的分页语法是这样的:
SELECT * FROM t_privilege_role limit 10 offset 10;
SELECT * FROM t_privilege_role offset 10 limit 10;
复制代码
乍一看如同一样,然而认真一看是有区别的。
postgreSQL的分页语句必须有limit和offset,并且地位能够调换,官网规范是limit ? offset ?写法;
MySQL的分页语句能够省略offset,不省略的状况下必须是limit在前offset在后;
所以下面debug中的SQL语句的分页条件是 OFFSET ? LIMIT ?。
因为我应用的是PageHelper插件,所以这个条件是插件帮我主动增加下来的。
那回到debug中的景象,为什么offset条件没有了呢?是不是在druid的SQLUtils解析SQL的时候出问题了呢?持着狐疑态度我又debug一遍,发现在SQLUtils解析完SQL后,分页条件就曾经变了。

也就是说,SQLUtils并没有解析出offset ? limit ?中的offset ?,因为SQLUtils是依照官网规范的分页语法进行解析的。
PageHelper的分页能改吗?
那PageHelper是怎么样来给查问语句加的分页条件呢?能不能让分页条件改成limit ? offset ?呢?
PageHelper减少分页条件的原理,其实也是应用了MyBatis的拦截器性能,在拦截器中依据用户设置的分页参数,增加上分页条件。
因为不同的数据库的分页语句语法存在差别,所以须要指定PageHelper应用哪个数据库的方言。咱们个别会在配置文件中进行指定。
// pagehelper方言,mysql,oracle,postgresql等
pagehelper.helperDialect=postgresql
复制代码
这个方言参数,最终在代码中是有一个具体的实现类。

如上图,在PageHelper中默认对这些数据库进行了反对。咱们来看一下postgreSQL的实现形式。

能够看到,对PostgreSqlDialect是继承了MySqlDialect,并且分页参数如果起始页不是0的话,应用的是offset ? limit ?,咱们顺便看一下MySqlDialect是如何实现的。

能够看出在MySqlDialect中依照MySQL的语法做了实现,并且起始页不为0时采纳的是limit ?,?语法。
由此,我权且判断一下,应该是pageHelper的开发者在前期对postgreSQL进行反对,偷懒了一下,间接继承了MySqlDialect,采纳了postgreSQL的另一种分页语法 offset ? limit ?。
为了证实我这个判断,我专门到pageHelper的github下来找看看有没有人提过相似的issue,你别说,还真找到了。

有人在应用mybatis-plus时,也会因为这个分页语法的问题导致分页性能呈现问题。
并且这位outian敌人曾经提交了批改代码,在 v5.3.1版本中解决了这个问题。
解决方案
如果你应用的pageHelper是v5.3.1版本以下,并且应用的数据库是postgreSQL,那你就要小心了,快去检查一下是不是踩了这个坑。
那要怎么解决呢?有两个方法:

降级PageHelper版本到v5.3.1+版本;
自定义PostgreSqlDialect;

当然,这两种形式当初来说都很好做,降级版本我不确定会不会有什么新的问题,如果你比拟大胆,能够降级一下做好功能测试;
这里次要说第二种形式,其实和下面那位解决这个问题的敌人根本一样。
首先自定义一个方言类,继承AbstractHelperDialect;
而后在类加载时,将这个方言类注册到PageHelper的方言中;
最初将这个方言配置到配置文件中pagehelper.helperDialect=customerpostgresql。

官网的修复办法也是将postgreSQLDialect依照这种形式批改。
最初
最初简略总结下,应用postgreSQL数据库,在应用PageHelper做分页时,如果PageHelper的版本低于5.3.1,那么在拦截器场景下会有问题,须要通过降级或者革新postgreSQLDialect的形式解决,如果你的我的项目中正在应用,能够提前排查,避免出现问题。
以上就是本期的全部内容,心愿对你有所帮忙。