MyBatis源码学习(二)

看完趣味数学书后,开始技术填坑之路


通常在业务中,需要进行数据分页查询,这样一来,每条SQL语句都加上limit限制,会多了很多重复的代码,而且每次需要自己在代码中进行偏移量的计算,略微有些麻烦。

还好有大神在Github里贡献了分页插件,而且使用起来很方便,了解了一下使用原理,发现是使用了MyBatis里面的拦截器Interceptor,学习记录一下。


简单🌰

项目中进行引用

一般JavaWeb项目使用到的是maven和gradle进行项目管理

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>最新版本</version>
</dependency>

···

// https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper
compile group: 'com.github.pagehelper', name: 'pagehelper', version: '5.1.4'

在mybatis-config或者spring中进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mybatis:
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>

spring:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:mapping/*.xml">
</property>
<!-- 配置分页插件 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<value>
reasonable=true
</value>
</property>
</bean>
</array>
</property>
</bean>

我是用的是第二种配置方式(Spring),更加细详细配置说明可以在Github文档中进行查看。

mapper查询语句

为了简单,使用了单表查询

1
2
3
4
5
6
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List" />
FROM course_user
WHERE user_id = #{userId, jdbcType=INTEGER}
</select>

代码使用

查询时,在代码中使用的是官网推荐的第二种实现方式,

1
2
3
4
5
6
7
8
// 使用这个方法,接下来的第一个select方法会进行分页
PageHelper.startPage(1, 5);
List<CourseUser> list = mapper.selectByUserId(userId);
// 查询结果list类型是Page<E>,要取出分页信息,可以强转,也可以使用PageInfo
// 用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
// 获取总数量
int count = page.getTotal();

分页插件,源码实现原理

该插件支持很多数据库,Oracle、MySQL、mongdb等都支持,一般常用的话是MySQL,测试用的项目也是使用MySQL作为持久层,该分页插件可以通过autoDialect自动方言适配了MySQL。

插件通过MyBatis的拦截器Interceptor进行SQL重写,可以从源码中看出

统计数量

拦截器入口:com.github.pagehelper.PageInterceptor#intercept

获取统计数量的方法:com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 获取智能的countSql
*
* @param sql
* @param name 列名,默认 0
* @return
*/
public String getSmartCountSql(String sql, String name) {
//解析SQL
Statement stmt = null;
//特殊sql不需要去掉order by时,使用注释前缀
if(sql.indexOf(KEEP_ORDERBY) >= 0){
return getSimpleCountSql(sql);
}
try {
stmt = CCJSqlParserUtil.parse(sql);
} catch (Throwable e) {
//无法解析的用一般方法返回count语句
return getSimpleCountSql(sql);
}
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
try {
//处理body-去order by
processSelectBody(selectBody);
} catch (Exception e) {
//当 sql 包含 group by 时,不去除 order by
return getSimpleCountSql(sql);
}
//处理with-去order by
processWithItemsList(select.getWithItemsList());
//处理为count查询
sqlToCount(select, name);
String result = select.toString();
return result;
}

通过该方法,去掉参数args,然后改成count(*),修改成统计数量的SQL。

进行分页查询

具体调用方法:com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
// 判断是不是第一页,如果是第一页,不需要设置偏移量
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ? ");
} else {
sqlBuilder.append(" LIMIT ?, ? ");
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}

从这里看出,该插件不是通过内存进行分页,而是通过修改SQL进行物理分页。


总结

感觉这个分页插件没有入侵代码,实现思想也很优雅,体现了拦截器思想,最后结果就不贴出来了,一般查询出来total总量和分页数据就足够了。

参考资料

1 分页插件地址Github