因为最近工作太忙,隔了好久,终于有时间写点文章了。闲话少说,进入本期正题。省流版:PagerHelper与PostgreSQL一起使用时,会有坑!如果你想知道这是一个什么样的坑,继续往下看,我给你解释一下。背景最近在做一个项目,常规的SpringBoot+Mybatis架构,数据库使用PostgreSQL,有一些场景需要用到分页查询,所以自然要用PageHelper这个分页工具,如果你是初学者或者还没有使用过,通过PageHelper,你可以了解一下。PageHelper此处一切顺利,但正如您一定猜到的那样聪明,有些事情即将发生。我需要完成一个数据权限的功能。根据每个用户的不同数据权限,每个用户只能查询有权限的用户。比如用户A只能访问自己组织的数据,用户B是高级用户可以查询多个组织的数据,用户C是超级管理员可以查询所有数据等等。我实现这个功能的方式是查询时根据用户的权限添加相应的查询条件。比如业务方法中的SQL是这样的:select*fromt_datawheredata_id=1limit10offset0我需要修改复制代码为:select*fromt_datawheredata_id=1anddata_partin(1,2,3)limit10offset0这里要强调一点,我不仅要给t_data表加条件,还要给其他很多表加条件,而且每个表加的条件可能不一样。我使用了MyBatis的拦截器功能,在拦截器中统一处理SQL,并添加数据权限条件。本以为一切都会如我所料,没想到中途踩了一个坑,折腾了半夜。以上就是踩坑的背景,接下来我们看代码。代码实现我在拦截器中这样做。MyBatis拦截器实现/**数据权限拦截器拦截所有MyBatis查询方法@作者小黑说@version1.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})})publicclassDataPermissionInterceptorimplementsInterceptor{privatestaticfinalLoggerlog=LoggerFactory.getLogger(DataPermissionInterceptor.class);@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{//这是一个预先处理好的当前用户数据权限的集合,存储在ThreadLocal中。Listconditions=DataConditionHelper.getConditions();//如果条件为空,表示用户没有数据权限限制,可以查询所有数据。if(CollectionUtils.isEmpty(conditions)){returninvocation.proceed();}对象[]args=invocation.getArgs();MappedStatementms=(MappedStatement)args[0];对象参数=args[1];RowBoundsrowBounds=(RowBounds)args[2];ResultHandlerresultHandler=(ResultHandler)args[3];执行器executor=(Executor)invocation.getTarget();缓存键缓存键;BoundSql绑定Sql;if(args.length==4){boundSql=ms.getBoundSql(参数);cacheKey=executor.createCacheKey(ms,parameter,rowBounds,boundSql);}else{cacheKey=(CacheKey)args[4];boundSql=(BoundSql)args[5];}//对当前Query语句进行数据权限的增强处理StringnewSql=handleSql(boundSql.getSql());//创建一个新的查询语句对象BoundSqlnewBoundSql=newBoundSql(ms.getConfiguration(),newSql,boundSql.getParameterMappings(),boundSql.getParameterObject());//将新的查询SQL放入语句MappedStatementnewMs=newMappedStatement(ms,newBoundSqlSqlSource(newBoundSql));对于(ParameterMapping映射:boundSql.getParameterMappings()){Stringprop=mapping.getProperty();如果(boundSql.hasAdditionalParameter(prop)){newBoundSql.setAdditionalParameter(prop,boundSql.getAdditionalParameter(prop));}}returnexecutor.query(newMs,parameter,rowBounds,resultHandler,cacheKey,newBoundSql);}/***增强型SQL*/privateStringhandleSql(StringorigSql){SQLExprsqlExpr=SQLUtils.toSQLExpr(origSql,JdbcConstants.POSTGRESQL);DataPermissionSelectVisitorselectVisitor=newDataPermissionSelectVisitor();sqlExpr.accept(selectVisitor);returnsqlExpr.toString();}@OverridepublicObjectplugin(Objecttarget){if(targetinstanceofPlugin.Executor){ret(target,this);}returntarget;}}CopycodeSQLenhancement在增强SQL的时候,我需要把原来的SQL语句解析成一个抽象语法树(AST),你可以理解为把SQL语句转换成一个java对象的树结构,使用Java对象来表示SQL中的所有元素,比如查询字段,WHERE,JOIN等,然后我需要添加条件已经有很多成熟的工具通过给表添加数据权限条件来解析SQL语句。您不需要自己开发它们。我这里使用的是阿里的druid包中的SQLUtils。关注我添加条件的代码。publicclassDataPermissionSelectVisitorextendsPGASTVisitorAdapter{privatestaticfinalStringTABLE_IGNORE_CHAR="\"|'|`";@Overridepublicbooleanvisit(SQLExprTableSourcex){//数据权限条件ListdataConditions=DataConditionHelper.getConditions();.isEmpty(dataConditions)){returnsuper.visit(x);}SQLNametableName=x.getName();Stringalias=x.getAlias();//存储可添加的条件sql片段ListconditionSqls=newArrayList<>();for(Conditioncondition:dataConditions){Stringname=tableName.getSimpleName().replaceAll(TABLE_IGNORE_CHAR,"");if(name.equals(condition.getTableName())){//在数据权限中如果表名与当前SQL中的表名相同,需要添加数据权限条件()));}else{conditionSqls.add(条件.buildCondition());}}}如果(CollectionUtils.isEmpty(conditionSqls)){返回super.visit(x);}SQLObjectparent=x.getParent();//单表查询if(parentinstanceofSQLSelectQueryBlock){SQLSelectQueryBlockquery=(SQLSelectQueryBlock)parent;//创建条件对象SQLExprexpr=buildExpr(conditionSqls);//添加条件对象到查询对象query.addCondition(expr);}//联合表查询if(parentinstanceofSQLJoinTableSource){SQLJoinTableSourcequery=(SQLJoinTableSource)parent;SQLExprexpr=buildExpr(conditionSqls);query.addConditionn(expr);}returnsuper.visit(x);}/***构建条件对象*/privateSQLExprbuildExpr(ListconditionSqls){SQLExprexpr=null;for(StringconditionSql:conditionSqls){if(expr==null){expr=SQLUtils.toSQLExpr(conditionSql,JdbcConstants.POSTGRESQL);}}else{SQLExprsqlExpr=SQLUtils.toSQLExpr(conditionSql,JdbcConstants.POSTGRESQL);expr=SQLUtils.buildCondition(SQLBinaryOperator.BooleanOr,expr,true,sqlExpr);}}返回表达式;JustprivateStringhandleSql(StringorigSql){//将SQL解析成SQLExpr对象SQLExprsqlExpr=SQLUtils.toSQLExpr(origSql,JdbcConstants.POSTGRESQL);//使用Visitor对象的accpet方法添加数据权限条件DataPermissionSelectVisitorselectVisitor=newDataPermissionSelectVisitor();sqlExpr.accept(selectVisitor);returnsqlExpr.toString();}复制代码到这里,貌似一切正常,我测试了几个单表查询和联表查询都没有问题。直到我测试了一个带分页功能的查询,第一页没有问题,正常返回。然而,从第二页开始,出问题了。在异常信息中,我标记了几个关键信息。从这个异常信息,基本可以猜到应该是SQL语句中的参数占位符和传入的参数个数不匹配导致的。一般遇到这种问题,我的第一反应是编程网络第一。显然,网友们已经提前踩过类似的坑了。看了一圈,基本可以肯定和我的猜测是一致的。接下来,让我们看一下调试。最初的想法是在使用分页查询的时候,看一下SQL语句在添加数据权限前后的变化。修改SQL的调试方法:privateStringhandleSql(StringorigSql){//SQLExpr在添加条件之前添加条件后returnsqlExpr.toString();}复制代码我们调试一下,在添加数据权限条件之前,分页查询语句如下:添加数据权限后,分页查询语句变成了这样this:搞什么鬼,怎么只剩下一个极限了?!offset被吃掉了?!冷静点,这个分页语句有问题。这里需要说明一下,postgreSQL分页语句的写法和MySQL有点不同。MySQL中分页语句的写法是:--MySQL分页语法SELECT*fromtableNamewhere1=1limit10offset10;SELECT*fromtableNamewhere1=1limit0,10;SELECT*fromtableNamewhere1=1limit10;复制代码但是postgreSQL的分页语法是这样的:SELECT*FROMt_privilege_rolelimit10offset10;SELECT*FROMt_privilege_roleoffset10limit10;复制代码初看是一样的,仔细看还是有区别的。PostgreSQL的分页语句必须有limit和offset,位置可以互换。官方标准是极限?抵消?;MySQL的分页语句可以省略偏移量。SQL语句中的分页条件是OFFSET?限制?。因为我用的是PageHelper插件,这个条件是插件自动帮我加上的。那么回到debug中的现象,为什么offset条件没有了呢?是不是druid的SQLUtils解析SQL有问题?抱着怀疑的态度,再次调试,发现SQLUtils解析SQL后,分页条件发生了变化。也就是说,SQLUtils没有解析出偏移量?在抵消?limit?,因为SQLUtils是按照官方标准的分页语法解析的。PageHelper的分页可以改吗?那么PageHelper是如何为查询语句添加分页条件的呢?分页条件可以改成limit吗?抵消??PageHelper添加分页条件的原理其实就是利用了MyBatis的拦截器功能。在拦截器中,根据用户设置的分页参数添加分页条件。因为不同数据库的分页语句语法不同,所以需要指定PageHelper使用的是哪种数据库方言。我们一般在配置文件中指定。//pagehelperdialect,mysql,oracle,postgresql等pagehelper.helperDialect=postgresql复制代码的dialect参数,最后代码中有具体的实现类。如上所示,PageHelper默认支持这些数据库。我们来看看postgreSQL是如何实现的。可以看出PostgreSqlDialect继承了MySqlDialect,如果分页参数不为0,偏移量呢?限制?用来。我们来看看MySqlDialect是如何实现的。可以看出,MySqlDialect是按照MySQL的语法实现的,而limit?,?起始页不为0时使用的语法,由此判断应该是pageHelper的开发者后期支持postgreSQL,偷懒,直接继承了MySqlDialect,采用了另一种分页语法offset?限制?的postgreSQL。为了证实我的判断,我去pageHelper的github上看看有没有人提到过类似的问题。别告诉我,我真的找到了。有的人在使用mybatis-plus的时候,也会因为这个分页语法问题导致分页功能出现问题。而这位欧天朋友已经在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来解决。如果你的正在项目中使用,可以提前检查,避免出现问题。以上就是本期的全部内容,希望对大家有所帮助。