Pagination и Sorting в Spring Data JDBC

В этой статье рассмотрим, как выводить данные постранично в Spring Data JDBC. Для этого предусмотрены интерфейсы Page (выведенная страница) и Pagable (для запроса страницы).

Рассмотрим пример.

Данные

Пусть в базе одна таблица animal.  Скрипт, выполняющийся при запуске приложения, schema.sql:

DROP TABLE IF EXISTS ANIMAL;
CREATE TABLE IF NOT EXISTS ANIMAL(
    ID BIGINT AUTO_INCREMENT PRIMARY KEY,
    NAME VARCHAR(255)
);

И data.sql:

insert into animal (name) values ('cat');
insert into animal (name) values ('dog');
insert into animal (name) values ('eagle');
insert into animal (name) values ('goat');
insert into animal (name) values ('cow');
insert into animal (name) values ('horse');

Класс Animal (в данном примере это не JPA-сущность, но вообще постраничный вывод можно делать и для JPA):

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Animal {
    @Id
    private long id;
    private String name;
}

PagingAndSortingRepository

В Spring существует интерфейс, методы которого будем использовать:

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

    Iterable<T> findAll(Sort sort);

    Page<T> findAll(Pageable pageable);
}

AnimalRepository

AnimalRepository расширяет его:

public interface AnimalRepository extends PagingAndSortingRepository<Animal, Long> {

}

Pagination

Для постраничного вывода элементов в параметр метода передается объект Pageable. Он содержит информацию о количестве элементов на странице и номере запрашиваемой страницы:

Pageable firstPageWithTwoElements = PageRequest.of(0, 2);

Страницы нумеруются с нуля. Выше показан запрос первой страницы, на каждой странице два элемента.

Методы постраничного выбора возвращают Page (либо List, как увидим ниже).

Page – объект, который помимо списка возвращаемых элементов, содержит общее число страниц, номер страницы и т.д.:

Page object
Page object

Вот так мы запрашиваем страницу Page:

Page<Animal> animals = dao.findAll(firstPageWithTwoElements);

Чтобы вернуть сам список, мы вызываем метод getContent() полученного Page:

@Test
void whenFindFirstPageWithTwoElements_thenOk() {

    Pageable firstPageWithTwoElements = PageRequest.of(0, 2);
    Page<Animal> animals = dao.findAll(firstPageWithTwoElements);

    Assertions.assertEquals(
            Arrays.asList(new Animal(1, "cat"), new Animal(2, "dog")),
            animals.getContent());
}

Поскольку класс Animal аннотирован Lombok-аннотацией @Data, в классе Animal метод equals() переопределен. А именно: два Animals равны, если их поля id и name равны.

А поскольку два списка List равны, если все их элементы равны и находятся в том же порядке, то списки мы можем сравнивать обычным assertEquals(). Что и делаем выше.

Теперь запросим вторую страницу:

@Test
void whenFindSecondPageWithTwoElements_thenOk() {

    Pageable secondPageWithTwoElements = PageRequest.of(1, 2);
    Page<Animal> animals = dao.findAll(secondPageWithTwoElements);

    Assertions.assertEquals(
            Arrays.asList(new Animal(3, "eagle"), new Animal(4, "goat")),
            animals.getContent());

}

Выбираются два элемента второй страницы.

Pagination и Sorting

В объекте Pageable можно задать заодно и сортировку. Так что элементы и сортируются, и выдаются постранично. Для этого используем третий параметр –  Sort:

PageRequest.of(0, 2, Sort.by("name").descending());

Проверим, что выдается первая страница отсортированных по имени (и по убыванию) элементов:

@Test
void givenSort_whenFindFirstPageWithTwoElements_thenOk() {
    Pageable firstPageWithTwoElementsSortedByNameDesc =
            PageRequest.of(0, 2, Sort.by("name").descending());

    Page<Animal> animals = dao.findAll(firstPageWithTwoElementsSortedByNameDesc);

    Assertions.assertEquals(
            Arrays.asList(new Animal(6, "horse"), new Animal(4, "goat")),
            animals.getContent());
}

Sorting

Теперь используем второй метод, который просто сортирует элементы (без постраничной выдачи):

Iterable<T> findAll(Sort sort);

Для этого зададим Sort:

Sort sort = Sort.by("name").ascending();

Выдается 6 упорядоченных по возрастанию элементов:

@Test
void givenSort_whenFindAll_thenOk() {
    Sort sort = Sort.by("name").ascending();

    Iterable<Animal> animals = dao.findAll(sort);

    Assertions.assertEquals(
            Arrays.asList(
                    new Animal(1, "cat"),
                    new Animal(5, "cow"),
                    new Animal(2, "dog"),
                    new Animal(3, "eagle"),
                    new Animal(4, "goat"),
                    new Animal(6, "horse")
            ),
            animals);
}

Производный от имени метода запрос

Можно так же добавить Pageable в производный от имени запрос:

public interface AnimalRepository extends PagingAndSortingRepository<Animal, Long> {

    List<Animal> findAllByNameContaining(String str, Pageable pageable);

}

Но в этом случае возвращать нужно исключительно List, а не Page, из-за бага в Spring Data JDBC (на май 2021).

В Spring Data JPA багов с постраничным выводом меньше, а возможностей больше. Для всех запросов можно вернуть как List, так и Page. Кроме того, можно добавить Pageable в пользовательские @Query.

Pagination и JdbcTemplate

Наконец, напишем запрос с помощью JdbcTemplate, чтобы элементы могли возвращаться постранично.

Для этого создадим интерфейс CustomAnimalRepository и репозиторий:

public class CustomAnimalRepositoryImpl implements CustomAnimalRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Page<Animal> getAnimals(Pageable pageable) {
       ....
    }
}

Включим его в наш AnimalRepository, как рассказано тут. Таким образом, AnimalRepository станет составным:

public interface AnimalRepository extends PagingAndSortingRepository<Animal, Long>, CustomAnimalRepository {

    List<Animal> findAllByNameContaining(String str, Pageable pageable);

}

И метод getAnimals() мы сможем вызывать из AnimalRepository.

По задумке getAnimals() просто возвращает animals – постранично.

Итак, напишем сам метод.

Возвратить нужно PageImpl – реализацию Page. Для этого нужно получить сам список элементов на странице и общее число элементов. Поэтому запросов два.

Общее число animals:

select count(*) from animal

Список элементов на странице:

select * from animal LIMIT размер_страницы OFFSET сколько_элементов_пропустить

Итого, метод getAnimals():

public class CustomAnimalRepositoryImpl implements CustomAnimalRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public Page<Animal> getAnimals(Pageable pageable) {
        String rowCountSql = "select count(*) from animal";

        int count = jdbcTemplate.queryForObject(
                rowCountSql, Integer.class);

        String querySql = "select * from animal " +
                "LIMIT " + pageable.getPageSize() + " " +
                "OFFSET " + pageable.getOffset();
        List<Animal> animals = jdbcTemplate.query(querySql, new AnimalMapper());


        return new PageImpl<>(animals, pageable, count);
    }
}

Класс AnimalMapper вспомогательный, он сопоставляет каждую полученную строку объекту Animal:

class AnimalMapper implements RowMapper<Animal> {
    @Override
    public Animal mapRow(ResultSet resultSet, int i) throws SQLException {
        final int id = resultSet.getInt("id");
        final String name = resultSet.getString("name");
        return new Animal(id, name);
    }
}

Вызов метода:

Pageable firstPageWithTwoElements = PageRequest.of(0, 2);

Page<Animal> animals = dao.getAnimals(firstPageWithTwoElements);

Assertions.assertEquals(
        Arrays.asList(new Animal(1, "cat"), new Animal(2, "dog")),
        animals.getContent());

Таким образом можно постранично выводить любой сложный запрос.

Итоги

Код примеров доступен на GitHub. Дальше рассмотрим pagination для JPA-сущностей.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *