Пейджинговая штука Mybatis

MyBatis

предисловие

Можно сказать, что разбиение по страницам является очень распространенной функцией. Большинство основных баз данных предоставляют методы физического разбиения по страницам, такие как ключевое слово limit Mysql, ключевое слово Oracle ROWNUM и т. д. Mybatis, как структура ORM, также предоставляет функцию разбиения по страницам. Мибатис в деталях.

Разбиение на страницы

1. Введение в RowBounds

Mybatis предоставляет класс RowBounds для обработки подкачки, а также внутри предоставляет два значения смещения и предела, которые используются для указания начальной позиции данных запроса и количества данных запроса:

public class RowBounds {

  public static final int NO_ROW_OFFSET = 0;
  public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
  public static final RowBounds DEFAULT = new RowBounds();

  private final int offset;
  private final int limit;

  public RowBounds() {
    this.offset = NO_ROW_OFFSET;
    this.limit = NO_ROW_LIMIT;
  }
}

По умолчанию начинается с нижнего индекса 0, а объем данных запроса — Integer.MAX_VALUE; если запрос не указывает RowBounds, RowBounds.DEFAULT по умолчанию:

  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

2. Использование RowBounds

Он также очень прост в использовании, вам нужно только знать общее количество записей, затем установить количество запросов, которые будут опрошены на каждой странице, посчитать, сколько запросов делится в общей сложности, а затем указать индекс через RowBounds. примерный код такой:

    public String rowBounds() {
        int pageSize = 10;
        int totalCount = blogRepository.countBlogs();
        int totalPages = (totalCount % pageSize == 0) ? totalCount / pageSize : totalCount / pageSize + 1;
        System.out.println("[pageSize=" + pageSize + ",totalCount=" + totalCount + ",totalPages=" + totalPages + "]");
        for (int currentPage = 0; currentPage < totalPages; currentPage++) {
            List<Blog> blogs = blogRepository.selectBlogs("zhaohui", new RowBounds(currentPage * pageSize, pageSize));
            System.err.println("currentPage=" + (currentPage + 1) + ",current size:" + blogs.size());
        }
        return "ok";
    }

pageSize количество каждого запроса, общее количество записей totalCount, общее количество запросов totalPages;

3. Анализ границ строк

Соответствующий код для Mybatis для обработки пейджинга находится в DefaultResultSetHandler, часть кода выглядит следующим образом:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    //跳过指定的下标Offset
    skipRows(resultSet, rowBounds);
    ////判定当前是否读取是否在limit内
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }
  
  //跳过指定的下标Offset
  private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
      if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
        rs.absolute(rowBounds.getOffset());
      }
    } else {
      for (int i = 0; i < rowBounds.getOffset(); i++) {
        if (!rs.next()) {
          break;
        }
      }
    }
  }
  
  //判定当前是否读取是否在limit内
  private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
  }

При обработке ResultSet необходимо пропустить указанный нижний индекс Offset.Здесь метод пропуска делится на два случая: resultSetType имеет тип TYPE_FORWARD_ONLY и resultSetType не является типом TYPE_FORWARD_ONLY.Mybatis также предоставляет настройку типа.Параметры включают:

  • FORWARD_ONLY: можно прокручивать только вперед;
  • SCROLL_SENSITIVE: может выполнять произвольную прокрутку вперед и назад, чувствительную к модификации;
  • SCROLL_INSENSITIVE: может выполнять произвольную прокрутку вперед и назад и чувствителен к модификации;
  • ПО УМОЛЧАНИЮ: значение по умолчанию FORWARD_ONLY;

Когда тип FORWARD_ONLY, может быть пройден только указанный нижний индекс, в то время как два других типа могут быть непосредственно расположены в указанном нижнем индексе с помощью абсолютного метода, а тип может быть указан следующим образом:

    <select id="selectBlogs" parameterType="string" resultType="blog" resultSetType="SCROLL_INSENSITIVE ">
        select * from blog where author = #{author}
    </select>

limit limit, записать количество записей, прочитанных в данный момент, через resultCount, записанный в ResultContext, а затем определить, достигнут ли лимит limit;

PagehelperРазбиение на страницы

Помимо официально предоставленного метода пейджинга RowBounds, чаще используется сторонний плагин Pagehelper, почему уже есть официально предоставленный метод пейджинга, и появляются сторонние плагины, такие как Pagehelper, основная причина в том, что RowBounds обеспечивает логическое разбиение по страницам, а Pagehelper обеспечивает физическое разбиение по страницам;

1. Использование Pagehelper

Pagehelper в основном используетПлагин Mybatisфункция, поэтому вам сначала нужно настроить класс плагина PageInterceptor при его использовании:

        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mysql" />
        </plugin>

helperDialect используется для указания диалекта, а mysql здесь используется для тестирования, для более подробной настройки параметра обратитесь к официальной документации:Mybatis-PageHelper; Как вызвать, Mybatis-PageHelper также предоставляет множество методов, здесь используется метод RowBounds, конкретный код точно такой же, как в приведенном выше примере кода, но из-за существования плагина метод его реализации изменился;

2. Анализ Pagehelper

Pagehelper перехватывает метод запроса Executor следующим образом:

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
}

надПлагин анализа MybatisПлагин использует технологию динамического прокси, при выполнении метода запроса Executor автоматически сработает метод вызова InvocationHandler, а в методе будет вызван метод перехвата PageInterceptor:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

Вы можете видеть, что соответствующие параметры args (4 или 6) окончательного запроса инкапсулированы в Invocation, включая класс RowBounds для разбиения по страницам; Pagehelper сопоставит смещение и ограничение в RowBounds с более мощным классом Page, Page содержит множество свойств. , вот краткий обзор связанных с RowBounds:

    public Page(int[] rowBounds, boolean count) {
        super(0);
        if (rowBounds[0] == 0 && rowBounds[1] == Integer.MAX_VALUE) {
            pageSizeZero = true;
            this.pageSize = 0;
        } else {
            this.pageSize = rowBounds[1];
            this.pageNum = rowBounds[1] != 0 ? (int) (Math.ceil(((double) rowBounds[0] + rowBounds[1]) / rowBounds[1])) : 0;
        }
        this.startRow = rowBounds[0];
        this.count = count;
        this.endRow = this.startRow + rowBounds[1];
    }

Смещение сопоставляется с startRow, а предел сопоставляется с pageSize; с соответствующими параметрами подкачки, а затем с помощью настроенного типа диалекта базы данных генерируются разные диалекты для генерации sql. Например, Mysql предоставляет класс MySqlRowBoundsDialect:

public String getPageSql(String sql, RowBounds rowBounds, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (rowBounds.getOffset() == 0) {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(rowBounds.getLimit());
        } else {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(rowBounds.getOffset());
            sqlBuilder.append(",");
            sqlBuilder.append(rowBounds.getLimit());
            pageKey.update(rowBounds.getOffset());
        }
        pageKey.update(rowBounds.getLimit());
        return sqlBuilder.toString();
    }

Ключевым словом физической подкачки mysql является Limit, и подкачка может быть достигнута путем предоставления смещения и предела;

Сравнение производительности

RowBounds использует логическое разбиение по страницам, а Pagehelper использует физическое разбиение по страницам;
логический пейджинг: Логическое пейджинг использует пейджинг курсора, преимущество в том, что все базы данных унифицированы, но недостатком является низкая эффективность; используя пейджинг с прокруткой ResultSet, так как ResultSet имеет курсор, вы можете использовать его метод next() для указания на следующую запись; конечно, вы также можете использовать Scrollable ResultSets (прокручиваемый набор результатов), чтобы быстро найти строку, указанную курсором, используя метод absolute() ResultSet;
Физический пейджинг: Сама база данных обеспечивает метод подкачки, такой как предел mysql, преимущество в высокой эффективности, недостаток в том, что разные базы данных имеют разные методы подкачки, которые нужно обрабатывать отдельно для каждой базы данных;

Ниже приведен тест запроса 100 фрагментов данных с использованием трех методов разбиения по страницам: логического разбиения по страницам, логического разбиения по страницам вперед и назад и физического разбиения по страницам с использованием druid для мониторинга и используемой базы данных mysql;

1. Логическая прокрутка страниц вперед

Поскольку он может прокручивать только вперед, чем дальше находятся предыдущие страницы, тем больше данных просматривается, и мониторинг выглядит следующим образом:


Хотя имеется всего 100 фрагментов данных, считанные данные составляют 550 строк, а производительность низкая;

2. Прокрутка вперед и назад с логическим пейджингом

Настроенный здесь resultSetType — SCROLL_INSENSITIVE, который можно быстро найти Мониторинг выглядит следующим образом:

3. Физический пейджинг

Настройте использование подключаемого модуля Pagehelper и монитора следующим образом:

Видно, что физическое разбиение на страницы преобладает по времени выполнения и количеству прочитанных строк;

Суммировать

В этой статье представлены два метода разбиения по страницам, RowBounds и Pagehelper, которые представляют логическое и физическое разбиение по страницам соответственно, и внутренняя реализация этих двух методов; наконец, в конце статьи проводится простой тест производительности.

образец кода

Github