闲话少说,进入本期内容的正题。

省流版本: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,那你就要小心了,快去检查一下是不是踩了这个坑。

那要怎么解决呢?有两个办法:

  1. 升级PageHelper版本到v5.3.1+版本;
  2. 自定义PostgreSqlDialect;

当然,这两种方式现在来说都很好做,升级版本我不确定会不会有什么新的问题,如果你比较大胆,可以升级一下做好功能测试;

这里主要说第二种方式,其实和上面那位解决这个问题的朋友基本一样。

首先自定义一个方言类,继承AbstractHelperDialect;

然后在类加载时,将这个方言类注册到PageHelper的方言中;

最后将这个方言配置到配置文件中pagehelper.helperDialect=customerpostgresql

官方的修复方法也是将postgreSQLDialect按照这种方式修改。

最后

最后简单总结下,使用postgreSQL数据库,在使用PageHelper做分页时,如果PageHelper的版本低于5.3.1,那么在拦截器场景下会有问题,需要通过升级或者改造postgreSQLDialect的方式解决,如果你的项目中正在使用,可以提前排查,避免出现问题。

PageHelper这种情况下有坑相关推荐

  1. PageHelper这种情况下有坑!

    背景 小黑最近在做一个项目,常规的SpringBoot+Mybatis架构,数据库使用的是PostgreSQL,并且有一些场景需要使用到分页查询,很自然的就使用了PageHelper这个分页工具,如果 ...

  2. PageHelper这种情况下有坑!注意别吃亏

    哈喽各位小伙伴~最近新做了一个项目,常规的SpringBoot+Mybatis架构,数据库使用的是PostgreSQL,并且有一些场景需要使用到分页查询,很自然的就使用了PageHelper这个分页工 ...

  3. PageHelper这种情况下有坑,注意别吃亏

    哈喽各位小伙伴~最近新做了一个项目,常规的SpringBoot+Mybatis架构,数据库使用的是PostgreSQL,并且有一些场景需要使用到分页查询,很自然的就使用了PageHelper这个分页工 ...

  4. 二手电脑用什么软件测试性能好坏,小白买二手电脑怎么检测避免被坑?在什么情况下又可以砍价?...

    原标题:小白买二手电脑怎么检测避免被坑?在什么情况下又可以砍价? 最近有朋友问到,想买个二手电脑,怎么去检测,才不会被坑,有时候我们确实会遇到想买一台高性能的电脑,但是手上资金又不充裕的情况,但是二手 ...

  5. LinkedIn领英在什么情况下容易被封,提前学习避免进坑

    领英在什么情况下容易被封 01.同一个人注册使用多个领英帐号. 02.多个人共同使用同一个领英帐号. 03.虚假资料注册领英账号,常见于注册领英账号的时候初始姓名随便填写或胡编乱造,注册时使用了网络虚 ...

  6. ES 在数据量很大的情况下(数十亿级别)如何提高查询效率?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | advanced-java 来源 | http ...

  7. (转)面试必备技能:JDK动态代理给Spring事务埋下的坑!

    一.场景分析 最近做项目遇到了一个很奇怪的问题,大致的业务场景是这样的:我们首先设定两个事务,事务parent和事务child,在Controller里边同时调用这两个方法,示例代码如下: 1.场景A ...

  8. 博士生在没有导师指导的情况下,该如何自己选题发 CVPR ?

    来源丨知乎 编辑丨极市平台 本文仅作为学术分享,侵删 导读 本文就"博士生没有导师指导,如何自己发CVPR?" 一问题撷英掇华,精选了高赞回答希望对大家有所助益. 知乎原址:htt ...

  9. 分析动态代理给Spring事务埋下的坑

    前言 Spring的声明式事务让我们不在编写获得连接.关闭连接.开启事务.提交事务.回滚事务等代码,通过一个简单的@Transactional注解,就让我们轻松进行事务处理.我们知道Spring事务基 ...

最新文章

  1. 为什么 Linux 和 macOS 不需要碎片整理
  2. 什么?强化学习竟然来源于心理学?
  3. 关于navicat连接oracle 报 ORA-12737 set CHS16GBK错误的解决方法
  4. 关于realarm210 realarmTest.apk不能直接安装问题解决方法
  5. boost::shared_mutex
  6. html输入参数,传递输入参数,通过Html.ActionLink
  7. 锋利的jQuery学习笔记(4)-DOM操作
  8. 2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_1-4系统的整体演示...
  9. ajax跨域.pdf,探秘ajax跨域请求.pdf
  10. Javassist学习文档
  11. 解决ubuntu下微信不能发图片的问题。
  12. LeetCode #179 - Largest Number
  13. Python - 如何用turtle库画一个微笑表情包
  14. 用SQL查询创建水平、垂直直方图
  15. “收藏本站” 的代码
  16. 清华大学数据挖掘课程幕课习题(第二章)
  17. 【STM32标准库】【自制库】0.96寸OLED显示屏(SSD1306)(3)显示字母和数字,汉字
  18. 使用JSONArray遇到的字符串转义问题
  19. 面对技术,你焦虑的是什么?
  20. 基于Redis实现微信抢红包功能

热门文章

  1. 计算机设备维修预算申,维修费用申请报告
  2. css 从右到左滚动,CSS 文字从左到右滚动 (右进左出)
  3. 一顿饭的时间,教你怎样快速使用 动态代理ip 做一个获取Steam 热销商品 的方法
  4. 判断南红价值,“红“的等级是关键
  5. php 粘性表单功能,php 粘性表单验证
  6. 基于卷积神经网络的乳腺肿瘤良恶性分类方法研究
  7. 阿里的CTR预测:Deep Interest Network
  8. 不要浪费一场好危机(丘吉尔)
  9. win10更新后局域网电脑无法共享打印机
  10. 防红直连php,全新网址缩短防封 QQ/微信防红 短网址生成系统PHP源码