В этой статье рассмотрим, как выводить данные постранично в 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:
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-сущностей.