MyBatis implements instances of physical paging

  • 2020-05-30 20:19:06
  • OfStack

The paging implemented by MyBatis using RowBounds is logical paging, that is, it queries all data records first, and then truncates the records according to offset and limit

In order to achieve physical paging at the database level without changing MyBatis's functional logic, you can write plugin to intercept MyBatis Executor's statementhandler and rewrite SQL to execute queries

The plug-in code below is for MySQL only

plugin code

package plugin;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;

 *  By intercepting <code>StatementHandler</code> the <code>prepare</code> Method, override sql The statement implements physical paging. 
 *  As usual, the type to be intercepted in the signature must be the interface. 
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class PaginationInterceptor implements Interceptor {
  private static final Log logger = LogFactory.getLog(PaginationInterceptor.class);
  private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
  private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
  private static String DEFAULT_PAGE_SQL_ID = ".*Page$"; //  To intercept ID( Regular match )

  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
    RowBounds rowBounds = (RowBounds) metaStatementHandler.getValue("delegate.rowBounds");
    //  Detach the proxy object chain ( Since the target class may be intercepted by multiple interceptors, resulting in multiple proxies, the original target class can be separated by the following two loops )
    while (metaStatementHandler.hasGetter("h")) {
      Object object = metaStatementHandler.getValue("h");
      metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
    //  Separation of the last 1 The target class of the proxy object 
    while (metaStatementHandler.hasGetter("target")) {
      Object object = metaStatementHandler.getValue("target");
      metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);

    // property in mybatis settings In-file configuration 
    Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");

    //  Set up the pageSqlId
    String pageSqlId = configuration.getVariables().getProperty("pageSqlId");
    if (null == pageSqlId || "".equals(pageSqlId)) {
      logger.warn("Property pageSqlId is not setted,use default '.*Page$' ");
      pageSqlId = DEFAULT_PAGE_SQL_ID;

    MappedStatement mappedStatement = (MappedStatement)
    //  Override only those that require paging sql Statements. through MappedStatement the ID Match, default override to Page At the end of the MappedStatement the sql
    if (mappedStatement.getId().matches(pageSqlId)) {
      BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
      Object parameterObject = boundSql.getParameterObject();
      if (parameterObject == null) {
        throw new NullPointerException("parameterObject is null!");
      } else {
        String sql = boundSql.getSql();
        //  rewrite sql
        String pageSql = sql + " LIMIT " + rowBounds.getOffset() + "," + rowBounds.getLimit();
        metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
        //  With physical paging, you don't need it mybatis Is paging out of memory, so reset the following two parameters 
        metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
        metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
    //  Hand over the power of execution 1 An interceptor 
    return invocation.proceed();

  public Object plugin(Object target) {
    //  When the target class is StatementHandler Type, the target class is wrapped, or the target itself is returned , Reduces the number of times the target is proxied 
    if (target instanceof StatementHandler) {
      return Plugin.wrap(target, this);
    } else {
      return target;

  public void setProperties(Properties properties) {
    //To change body of implemented methods use File | Settings | File Templates.


Configuration plugin

    <plugin interceptor="plugin.PaginationInterceptor" />

Query SQL

  <!--  Test paging query  -->
  <select id="selectUserByPage" resultMap="dao.base.userResultMap">
    SELECT * FROM user

Invoke the sample

  public List<User> selectUserByPage(int offset, int limit) {
    RowBounds rowBounds = new RowBounds(offset, limit);
    return getSqlSession().selectList("dao.userdao.selectUserByPage", new Object(), rowBounds);

In addition, in combination with Spring MVC, write page turning and generate page code

The page number class

package util;

 * Created with IntelliJ IDEA.
 * User: zhenwei.liu
 * Date: 13-8-7
 * Time:  In the morning 10:29
 * To change this template use File | Settings | File Templates.
public class Pagination {
  private String url; //  The page number url
  private int pageSize = 10; //  Displays the number of records per page 
  private int currentPage = 1;  //  The current page number 
  private int maxPage = Integer.MAX_VALUE;  //  The largest number of pages 

  //  To obtain offset
  public int getOffset() {
    return (currentPage - 1) * pageSize;

  //  To obtain limit
  public int getLimit() {
    return getPageSize();

  public String getUrl() {
    return url;

  public void setUrl(String url) {
    this.url = url;

  public int getPageSize() {
    return pageSize;

  public void setPageSize(int pageSize) {
    this.pageSize = pageSize;

  public int getCurrentPage() {
    return currentPage;

  public void setCurrentPage(int currentPage) {
    if (currentPage < 1)
      currentPage = 1;
    if (currentPage > maxPage)
      currentPage = maxPage;
    this.currentPage = currentPage;

  public int getMaxPage() {
    return maxPage;

  public void setMaxPage(int maxPage) {
    this.maxPage = maxPage;

To calculate the maximum page number, you need to know the total number of records in the data table, query SQL below

  <!--  The total number of records  -->
  <select id="countUser" resultType="Integer">

  public Integer countTable() {
    return getSqlSession().selectOne("dao.userdao.countUser");

Used in Controller

  public String getUserByPage(@RequestParam
                  int page, Model model) {
    pagination.setMaxPage(userDao.countTable() / pagination.getPageSize() + 1);
    List<User> userList = userDao.selectUserByPage(
        pagination.getOffset(), pagination.getLimit());
    return "index";

Related articles: