Перейти к содержанию

Пагинация

Пагинация в tarantool-spring-data

Пагинация в tarantool-spring-data поддерживает интерфейсы Slice<T>,Page<T>,Pageable. Вместо offset-based пагинации, которую не поддерживает tarantool/crud, интерфейсы реализуют cursor-based пагинацию.

Pageable

Интерфейс Pageable представляет параметры страницы: номер, размер.

Для использования пагинации в tarantool-spring-data интерфейс Pageable был расширен интерфейсом TarantoolPageable. Теперь настройки страницы дополнительно содержат кортеж-курсор (параметр after), относительно которого считывается страница, а также объект направления пагинации PaginationDirection.

Для создания экземпляра Pageable должны использоваться конструкторы класса TarantoolPageRequest. Есть два варианта создания TarantoolPageable:

  • new TarantoolPageRequest<>(int pageSize) - постраничная выборка с начала данный в space. Аргумент pageSize - размер страницы.
  • new TarantoolPageRequest<T>(int pageNumber, int pageSize, T cursor) - постраничная выборка данных, начиная с некоторой страницы. Здесь нужно учесть соответствие между номером страницы (начинаются с 0) и переданным курсором.

В качестве примера рассмотрим space (таблицу), в котором хранится 100 записей, удовлетворяющих вашему запросу на постраничную выборку. Вы хотите получать по 10 записей на страницу, начиная со второй страницы (pageNumber = 1). Для этого стоит передать следующие параметры:

TarantoolPageable<Person> pageable = new TarantoolPageRequest<>(1, 10, domainClassCursor);

где domainClassCursor - это объект доменного класса (в примере класс Person), который является кортежем-курсором (см. параметр after).

Пример класса модели данных:

@NoArgsConstructor
@AllArgsConstructor
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id
@Data
@KeySpace("person")
public class Person {

  @Id
  @JsonProperty("id")
  private Integer id;

  @Field("is_married")
  @JsonProperty("isMarried")
  private Boolean isMarried;

  @JsonProperty("name")
  private String name;
}

Page

При вызове методов с Page выполняются два запроса:

  1. Запрос на выборку данных. Если данных нет, возвращается Page<T> с Unpaged pageable.
  2. Если данные есть, создается запрос на вычисление общего количества записей в space, удовлетворяющих условиям (метод count()). На основе общего количества записей и размера страницы (передан в TarantoolPageRequest при создании) определяется общее количество страниц (максимальный номер страницы).

Важно: при параллельной работе с постраничной пагинацией и манипулированием данными (например, удаление или добавление записей) значение количества страниц может измениться.

Таким образом:

  1. При движении вперед страница существует (не пустая, и Pageable не является Unpaged), если получены данные и номер страницы не превышает максимальный номер.
  2. При движении назад страница существует, если получены данные и pageNumber >= 0.

Slice

При вызове методов с Slice запрашивается n + 1 записей, где n - размер среза. Если получено n + 1 записей, то следующий срез существует.

Таким образом:

  1. При движении вперед срез существует (не пустой, и Pageable не является Unpaged), если получены данные.
  2. При движении назад срез существует, если получены данные и pageNumber >= 0.

Особенности при несоответствии кортежа-курсора и номера страницы

Рассмотрим пример, когда кортеж-курсор соответствует кортежу, после которого начинается страница (см. параметр after) с номером 0 (pageNumber = 0 (т.е. передан null)), а в конструкторе TarantoolPageRequest указан номер для другой страницы, например второй (pageNumber = 1):

                                                         (0)    (1)    (2)    (3)
Реальное разбиение данных в space:                     |-----||-----||-----||-----|

                                                    [0]    [1]    [2]    [3]
Разбиение, исходя из переданных параметров:     |-----||-----||-----||-----|

                                                               (вперед)
                                                       |------------------->
                                                    [0]    [1]    [2]    [3]
Page:                                           |--X--||-----||-----||-----|
                                                <--------------------------|
                                                               (назад)

                                                               (вперед)
                                                       |------------------------->
                                                  [0]    [1]    [2]    [3]   [4](3)
Slice:                                          |--X--||-----||-----||-----||-----|
                                                <---------------------------------|
                                                               (назад)

В данном примере происходит следующее:

Максимальный номер страницы - [3] (ваша нумерация). Он соответствует странице с номером (2) при реальном разбиении данных.

  • Page:
    • При движении вперед последняя существующая страница (не пустая, и Pageable не является Unpaged) будет иметь номер [3] ((2) - при реальном разбиении на страницы).
    • При движении назад последняя существующая страница будет иметь номер [1], но при этом метод hasPrevious() вернет значение true у этой страницы. При дальнейшем движении вы получите одну пустую страницу с Unpaged pageable.
  • Slice:
    • При движении вперед последний существующий срез (не пустой, и Pageable не является Unpaged) будет иметь номер [4] ((3) - при реальном разбиении на страницы).
    • При движении назад последний существующий срез будет иметь номер [1], но при этом метод hasPrevious() вернет значение true у этого среза. При дальнейшем движении вы получите один пустой срез с Unpaged pageable.

Рассмотрим пример, когда кортеж-курсор соответствует кортежу, после которого начинается страница (см. параметр after) с номером 1 (pageNumber = 1), а в конструкторе TarantoolPageRequest указан номер для другой страницы, например 1 (pageNumber = 0):

                                                  (0)    (1)    (2)    (3)
Реальное разбиение данных в БД:                 |-----||-----||-----||-----|

                                                         [0]    [1]    [2]    [3]
Разбиение, исходя из переданных параметров:            |-----||-----||-----||-----|

                                                               (вперед)
                                                       |-------------------------->
                                                         [0]    [1]    [2]    [3]
Page:                                                  |-----||-----||-----||--X--|
                                                       <-------------------|
                                                                 (назад)

                                                                 (вперед)
                                                       |------------------->
                                                         [0]    [1]    [2]
Slice:                                                 |-----||-----||-----|
                                                       <-------------------|
                                                              (назад)

В данном примере происходит следующее:

Максимальный номер страницы - [3] (ваша нумерация). Он соответствует номеру несуществующей страницы при реальном разбиении данных:

  • Slice:

    • При движении вперед последний существующий срез будет иметь номер [2].
    • При движении назад последний существующий срез будет иметь номер [0]((1) - при реальном разбиении на страницы).
  • Page:

    • При движении вперед последняя существующая страница (не пустая, и Pageable не является Unpaged) будет иметь номер [2], но при этом метод hasNext() вернет значение true у этой страницы. При дальнейшем движении вы получите одну пустую страницу с Unpaged pageable.
    • При движении назад последняя существующая страница будет иметь номер [0] ((1) - при реальном разбиении на страницы).

Работа с производными методами

При работе с производными методами существует особенность использования запросов с пагинацией.

  • Если поле-предикат является полем, для которого построен индекс, то поиск элементов будет происходить по этому индексу. Это означает, что при движении вперед возвращаемые на страницах записи будут идти в порядке возрастания индекса, при движении назад в порядке убывания индекса (используйте средства Java для сортировки элементов страниц).
  • Если поле-предикат не является полем, для которого построен индекс, то поиск элементов будет происходить по первичному индексу. Возвращаемые на страницах записи будут идти в порядке возрастания первичного индекса вне зависимости от направления пагинации.
Важно
  • Соблюдайте соответствие номера страницы и передаваемого курсора. В ином случае возможны пустые страницы с unpaged pageable. Если не уверены в правильности передачи курсора вместе с номером страницы, то используйте конструктор new TarantoolPageRequest<>(int pageSize), который позволяет пройтись от начала данных и не имеет недостатков присущих пагинации, начинающейся с произвольной страницы.
  • Если вы работаете на версии Spring Data 3.1.x или выше, используйте Scroll API для реализации запросов с пагинацией вместо Slice и Page.