手寫MyBatis分頁插件

前言

在開發查詢類的接口時,有一個讓開發者比較頭疼的問題:分頁。
如果每次都要開發者自己去寫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分頁失效的問題。
感興趣的同學可以基於這個思路繼續優化!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章