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