新版本的PageHelper与mybatis请移步这里——支持PageHelper5.1.4 Mybatis3.4.6
先说说使用PageHelper踩过的坑:
- 在mapper映射文件中,如果使用了limit关键字,而又使用了PageHelper的功能,比如orderBy,那么就会报错。
- 在使用稍微复杂的查询时,PageHelper是针对最外层(最下方)的数据进行分页的。
出现这样的问题,归根结底还是PageHelper源码的问题。
不多说PageHelper本身,本文使用的PageHelper版本为
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>4.2.1version>
dependency>
这里以MySql为例,使得PageHelper支持复杂查询的分页。
先看看原版的方言类的定义
package com.github.pagehelper.dialect;
@Override
public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
从这里可以看出,PageHelper在针对MySql数据库进行分页时,是在sql语句的最后加上limit偏移量进行分页查询的。且不说这种查询在数据量大时效率可能不高,这样去操作的话,在稍微复杂的查询中,通常无法正常的进行分页。
/**
* Created by Anur IjuoKaruKas on 2017/9/28.
* Description :
*/
public class CustomDialect extends MySqlDialect {
public CustomDialect(SqlUtil sqlUtil) {
super(sqlUtil);
}
@Override
public Object processPageParameter(MappedStatement ms, Map paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
return super.processPageParameter(ms, paramMap, page, boundSql, pageKey);
}
@Override
public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
if (sql.indexOf(ProjectConstant.SQL_SIGN) != -1) {
StringBuffer stringBuffer = new StringBuffer(sql);
stringBuffer.indexOf(ProjectConstant.SQL_SIGN);
StringBuffer mae = new StringBuffer(stringBuffer.substring(0, stringBuffer.indexOf(ProjectConstant.SQL_SIGN)));
StringBuffer uShiRo = new StringBuffer(stringBuffer.substring(stringBuffer.indexOf(ProjectConstant.SQL_SIGN), sql.length()));
mae.insert(mae.lastIndexOf(")")," limit ?,?");
return mae.append(uShiRo).toString();
}
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
}
所以,在这里,我对它进行了一点点的改写。
新建一个CustomDialect继承MySqlDialect,并重写了原方法。
在这里,做了一个判断,如果sql语句中包含 * ProjectConstant.SQL_SIGN(limitable) ,那么在这前面加上 “limit ?,?”*
这个SQL_SIGN是什么呢?
public static final String SQL_SIGN = "AS limitable";
实际上就是一个用于标记哪里需要使用偏移量的一个标识。
在mapper映射文件中就可以这样写:
<select id="selectRecordByComplexCondition" resultMap="ListQueryMap" parameterMap="ListQueryPo">
SELECT
*
from
(
SELECT
*
FROM
record r
WHERE
r.record_release_state = 1
<if test="tagIdListToSqlString!=null">
AND r.record_id IN (
SELECT
object_id
FROM
record_tag rtag
WHERE
rtag.tag_id IN ${tagIdListToSqlString}
AND rtag.module = #{moduleEnumIndex}
GROUP BY
rtag.object_id
HAVING
count(*) >= #{tagCount}
)
if>
AND r.module = #{moduleEnumIndex}
<if test="rcId!=null">
AND r.rc_id = #{rcId}
if>
<if test="raId!=null">
AND r.ra_id = #{raId}
if>
<if test="recordTitle!=null">
AND r.record_title LIKE CONCAT(CONCAT('%', #{recordTitle}), '%')
if>
<if test="recordReleaseTimeFrom!=null">
AND r.record_release_time > #{recordReleaseTimeFrom}
if>
/* 现在的PageHelper limit 加在这里 */
) AS limitable
LEFT JOIN record_tag rt ON rt.module = #{moduleEnumIndex}
AND limitable.record_id = rt.object_id
LEFT JOIN tag t ON rt.tag_id = t.tag_id
/* 原来的PageHelper limit 加在这里 */
select>
在这个业务中,我们想要进行分页的表是record,但如果不改mybatis,那么PageHelper将会对联表查询后的数据进行分页,那并不是我们想要的。
/**
* Created by Stack on 2017/9/28.
* Description :
*/
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class CustomPageHelper extends PageHelper {
private final CustomSqlUtil sqlUtil = new CustomSqlUtil();
@Override
public Object intercept(Invocation invocation) throws Throwable {
return sqlUtil.intercept(invocation);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
sqlUtil.setProperties(properties);
}
}
/**
* Created by Anur IjuoKaruKas on 2017/9/28.
* Description : 自定义sql处理工具
*/
public class CustomSqlUtil extends SqlUtil {
private Dialect dialect;
private Field additionalParametersField;
private Properties properties;
protected boolean autoDialect = true; //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
protected boolean autoRuntimeDialect; //运行时自动获取dialect
protected boolean closeConn = true; //多数据源时,获取jdbcurl后是否关闭数据源
private Map urlDialectMap = new ConcurrentHashMap(); //缓存
private ReentrantLock lock = new ReentrantLock();
/**
* 真正的拦截器方法
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
return doIntercept(invocation);
} finally {
BaseSqlUtil.clearLocalPage();
}
}
/**
* 真正的拦截器方法
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object doIntercept(Invocation invocation) throws Throwable {
//获取拦截方法的参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
RowBounds rowBounds = (RowBounds) args[2];
List resultList;
if (autoDialect) {
lock.lock();
try {
if (autoDialect) {
autoDialect = false;
this.dialect = getDialect(ms);
}
} finally {
lock.unlock();
}
}
Dialect runtimeDialect = dialect;
if (autoRuntimeDialect) {
runtimeDialect = getDialect(ms);
}
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {
ResultHandler resultHandler = (ResultHandler) args[3];
//当前的目标对象
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameterObject);
//反射获取动态参数
Map additionalParameters = (Map) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql);
countKey.update("_Count");
MappedStatement countMs = msCountMap.get(countKey);
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms);
msCountMap.put(countKey, countMs);
}
//调用方言获取 count sql
String countSql = runtimeDialect.getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
List countTempParameterMappingList = new ArrayList<>();
//**************** 截取有效部分进行count操作 by Anur 17/10/25
if (countSql.contains(BmsConstant.SQL_SIGN)) {
Integer subIndex = countSql.indexOf(BmsConstant.SQL_SIGN) + BmsConstant.SQL_SIGN.length();// 截取位置 到AS limitable
countSql = countSql.substring(0, subIndex);// 进行截取
Integer usefulParamCount = 0;// 参数舍弃
for (int i = 0; i < countSql.length(); i++) {
if (countSql.indexOf("?", i) != -1) {
usefulParamCount++;
i = countSql.indexOf("?", i);
}
}
List allCountTempParameterMappingList = boundSql.getParameterMappings();
countTempParameterMappingList = new ArrayList<>();
for (Iterator iterator = allCountTempParameterMappingList.iterator(); iterator.hasNext(); ) {
if (usefulParamCount == 0) {
break;
}
usefulParamCount--;
ParameterMapping next = iterator.next();
countTempParameterMappingList.add(next);
}
} else {
countTempParameterMappingList = boundSql.getParameterMappings();
}
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, countTempParameterMappingList, parameterObject);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);// 执行 count 查询
Long count = (Long) ((List) countResultList).get(0);// 处理查询总数
runtimeDialect.afterCount(count, parameterObject, rowBounds);
if (count == 0L) { // 当查询总数为 0 时,直接返回空的结果
return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);
}
}
List pageTempParameterMappingList;
// 判断是否需要进行分页查询
if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {
CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);// 生成分页的缓存 key
parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);// 处理参数对象
String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);// 调用方言获取分页 sql
//**************** 参数调度 by Anur 17/10/25
if (pageSql.contains(BmsConstant.SQL_SIGN)) {
String sqlInCount = pageSql.substring(0, pageSql.indexOf(BmsConstant.SQL_SIGN));
Integer count = -2;
for (int i = 0; i < sqlInCount.length(); i++) {
if (sqlInCount.indexOf("?", i) != -1) {
i = sqlInCount.indexOf("?", i);
count++;
}
}
List tempParameterMappingList = boundSql.getParameterMappings();
ParameterMapping parameterFirst = tempParameterMappingList.get(tempParameterMappingList.size() - 2);
ParameterMapping parameterSecond = tempParameterMappingList.get(tempParameterMappingList.size() - 1);
tempParameterMappingList.add(0 + count, parameterFirst);
tempParameterMappingList.add(1 + count, parameterSecond);
tempParameterMappingList.remove(tempParameterMappingList.size() - 2);
tempParameterMappingList.remove(tempParameterMappingList.size() - 1);
pageTempParameterMappingList = tempParameterMappingList;
} else {
pageTempParameterMappingList = boundSql.getParameterMappings();
}
//****************
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, pageTempParameterMappingList, parameterObject);
//设置动态参数
for (String key : additionalParameters.keySet()) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行分页查询
resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
resultList = new ArrayList();
}
} else {
args[2] = RowBounds.DEFAULT;
resultList = (List) invocation.proceed();
}
//返回默认查询
return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);
}
/**
* 初始化 dialect
*
* @param dialectClass
* @param properties
*/
private Dialect initDialect(String dialectClass, Properties properties) {
Dialect dialect;
try {
dialect = new CustomDialect(this);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("初始化 dialect [" + dialectClass + "]时出错:" + e.getMessage());
}
dialect.setProperties(properties);
return dialect;
}
/**
* 获取url
*
* @param dataSource
* @return
*/
@Override
public String getUrl(DataSource dataSource) {
Connection conn = null;
try {
conn = dataSource.getConnection();
return conn.getMetaData().getURL();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (conn != null) {
try {
if (closeConn) {
conn.close();
}
} catch (SQLException e) {
//ignore
}
}
}
}
/**
* 根据datasource创建对应的sqlUtil
*
* @param ms
*/
@Override
public Dialect getDialect(MappedStatement ms) {
//改为对dataSource做缓存
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
String url = getUrl(dataSource);
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
try {
lock.lock();
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
if (StringUtil.isEmpty(url)) {
throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialectStr = BaseSqlUtil.fromJdbcUrl(url);
if (dialectStr == null) {
throw new RuntimeException("无法自动获取数据库类型,请通过 dialect 参数指定!");
}
Dialect dialect = initDialect(dialectStr, properties);
urlDialectMap.put(url, dialect);
return dialect;
} finally {
lock.unlock();
}
}
@Override
public void setProperties(Properties properties) {
super.setProperties(properties);
//多数据源时,获取jdbcurl后是否关闭数据源
String closeConn = properties.getProperty("closeConn");
//解决#97
if (StringUtil.isNotEmpty(closeConn)) {
this.closeConn = Boolean.parseBoolean(closeConn);
}
//数据库方言
String dialect = properties.getProperty("dialect");
String runtimeDialect = properties.getProperty("autoRuntimeDialect");
if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
this.autoRuntimeDialect = true;
this.autoDialect = false;
this.properties = properties;
} else if (StringUtil.isEmpty(dialect)) {
autoDialect = true;
this.properties = properties;
} else {
autoDialect = false;
this.dialect = initDialect(dialect, properties);
}
try {
//反射获取 BoundSql 中的 additionalParameters 属性
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
具体的原理就不详细说了,这里稍微分析了一点点。
// 参数调度
if (pageSql.indexOf(ProjectConstant.SQL_SIGN) != -1) {
String sqlInCount = pageSql.substring(0, pageSql.indexOf(ProjectConstant.SQL_SIGN));
Integer count = 0;
for (int i = 0; i < sqlInCount.length(); i++) {
if (sqlInCount.indexOf("?", i) != -1) {
i = sqlInCount.indexOf("?", i);
count++;
}
}
List tempParameterMappingList = boundSql.getParameterMappings();
ParameterMapping parameterFirst = tempParameterMappingList.get(tempParameterMappingList.size() - 2);
ParameterMapping parameterSecond = tempParameterMappingList.get(tempParameterMappingList.size() - 1);
tempParameterMappingList.add(0 + count, parameterFirst);
tempParameterMappingList.add(1 + count, parameterSecond);
tempParameterMappingList.remove(tempParameterMappingList.size() - 2);
tempParameterMappingList.remove(tempParameterMappingList.size() - 1);
parameterMappingList = tempParameterMappingList;
} else {
parameterMappingList = boundSql.getParameterMappings();
}
基于PageHelper最大的改动是这里,在填充参数时,它的原本顺序是 ?,?,?,?,?,?,查询的位置,偏移量
但由于我们在前面吧 limit ?,? 移到了前面,所以这里也必须进行同步修改,实际上就是把这两个参数,根据sql语句的不同,提到前面去。
其他的也有一点点改动,比如说方言不再自动获取,直接使用我们上面自己定义的这个CustomDialect
@Configuration
public class MybatisConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setTypeAliasesPackage(MODEL_PACKAGE);
// 配置分页插件,详情请查阅官方文档
CustomPageHelper customPageHelper = new CustomPageHelper();
Properties properties = new Properties();
properties.setProperty("pageSizeZero", "true");//分页尺寸为0时查询所有纪录不再执行分页
properties.setProperty("reasonable", "true");//页码<=0 查询第一页,页码>=总页数查询最后一页
properties.setProperty("supportMethodsArguments", "true");//支持通过 Mapper 接口参数来传递分页参数
customPageHelper.setProperties(properties);
// 添加插件
factory.setPlugins(new Interceptor[]{customPageHelper});
// 添加XML目录
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
return factory.getObject();
}
直接沿用原来的配置就可以,只需对一个地方进行细微的修改,那就是在plugin这里,需要将pageHelper更换成自己定义的这个CustomPageHelper
6 <configuration>
7 <plugins>
8 <plugin interceptor="com.xxxx.xxxxx.CustomPageHelper">
9
10 <property name="dialect" value="mysql"/>
11
12
13
14 <property name="offsetAsPageNum" value="true"/>
15
16
17 <property name="rowBoundsWithCount" value="true"/>
18
19
20 <property name="pageSizeZero" value="true"/>
21
22
23
24 <property name="reasonable" value="true"/>
25
26
27
28
29 <property name="params" value="pageNum=start;pageSize=limit;"/>
30
31 <property name="supportMethodsArguments" value="true"/>
32
33 <property name="returnPageInfo" value="check"/>
34 plugin>
35 plugins>
36 configuration>
如果配置文件是xml的,也是同样进行修改,将interceptor改成你自己定义的这个。
PageHelper.startPage(pageNum,pageSize);
和之前没有任何区别,你只需要在需要sql语句中需要进行偏移的地,做如下修改
需要分页的地方套上一层外衣 SELECT (xxxxxxxxxxxxx)AS limitable,就可以实现分页了
大功告成~ 这样的修改,在不会对正常的分页产生影响~
不多说~ 这样可以根据自己的需求,随意对PageHelper进行修改,或者自己写一个低配版但是贴合需求的PageHelper。