前言
在開發查詢類的接口時,有一個讓開發者比較頭疼的問題:分頁。
如果每次都要開發者自己去寫limit,計算起始行和偏移量就太煩了,於是市面上誕生了一些優秀的分頁插件,例如:PageHelper。
PageHelper使用起來非常簡單,如下示例代碼:
Page page = PageHelper.startPage(1, 10);
//sql: select * from table
mapper.selectList();
調用PageHelper.startPage(1, 10)方法後,在執行SQL時會自動加上limit分頁,如下:
select * from table limit 0,10;
查看源碼會發現PageHelper.startPage(1, 10)
只做了一件事:構建Page實例存放到本地線程中。
它能修改執行的SQL語句得益於PageInterceptor
類,基於Mybatis提供的Interceptor插件來實現的。
本篇博客並不想分析PageHelper的源碼,感興趣的同學可以自己去研究一下。
MyBatis插件
不管是PageHelper還是其他分頁插件,實現的思路都是一樣的,給予MyBatis提供的Interceptor插件機制,允許開發者在映射語句執行過程中進行方法的攔截,可以動態修改入參、SQL語句、返回結果等。
MyBatis允許被攔截的方法包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
詳情請看官方文檔:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
手寫分頁插件
借鑑PageHelper的思路,自己手寫一個分頁插件,其實並不難。
Page類存放當前頁和大小
//省略getter setter
public class Page {
private Integer page;
private Integer size;
}
PageUtil通過ThreadLocal傳遞Page實例
public class PageUtil {
// Page通過ThreadLocal傳遞
static final ThreadLocal<Page> THREAD_LOCAL_PAGE = new ThreadLocal<>();
public static Page startPage(Integer page, Integer size) {
Page pageResult = new Page();
pageResult.setPage(page);
pageResult.setSize(size);
THREAD_LOCAL_PAGE.set(pageResult);
return pageResult;
}
public static Page localPage() {
return THREAD_LOCAL_PAGE.get();
}
public static void remove() {
THREAD_LOCAL_PAGE.remove();
}
}
PagePlugin,基於Interceptor實現的分頁邏輯
// 攔截StatementHandler類的prepare方法
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})})
public class PagePlugin implements Interceptor {
private Properties properties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Page page = PageUtil.localPage();
//沒有調用PageUtil.startPage(),則不分頁
if (page == null) {
return invocation.proceed();
}
//取出原sql,根據page拼接分頁sql
RoutingStatementHandler target = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = target.getBoundSql();
String sql = boundSql.getSql();
int limit = (page.getPage() - 1) * page.getSize();
ReflectUtil.setFieldValue(boundSql, "sql", sql + " limit " + limit + "," + page.getSize());
//刪除當前線程的page實例
PageUtil.remove();
//執行目標方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
配置插件
<plugins>
<plugin interceptor="com.ch.plugins.PagePlugin"></plugin>
</plugins>
Mapper接口
public interface UserMapper {
@Select("select * from user")
List<User> list();
}
分頁測試類
public class PageTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
Configuration configuration = sqlSessionFactory.getConfiguration();
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第1頁,每頁10條
Page page = PageUtil.startPage(1, 10);
for (User user : mapper.list()) {
System.out.println(user);
}
session.close();
} finally {
IoUtil.close(inputStream);
}
}
}
執行後,控制檯輸出日誌
DEBUG [main] - ==> Preparing: select * from user limit 0,10
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 10
分頁成功!
總結
主要記的是思路,分頁插件寫的比較簡陋,只是簡單的拼接SQL,也沒有考慮到limit分頁失效的問題。
感興趣的同學可以基於這個思路繼續優化!!!