В этой статье рассмотрим, как выводить данные постранично в Spring Data JPA. В предыдущей статье по Spring Data JDBC уже рассмотрены интерфейсы Page (выведенная страница) и Pagable (для запроса страницы). Здесь они используются аналогично.
Рассмотрим пример. Он аналогичный, но Animal теперь — JPA-сущность. Кроме того, добавлена сущность Category, чтобы показать более сложный запрос с group by, для которого тоже работает pagination.
Данные
Скрипт, выполняющийся при запуске приложения, schema.sql:
DROP TABLE IF EXISTS CATEGORY; CREATE TABLE IF NOT EXISTS CATEGORY( ID BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(255) ); DROP TABLE IF EXISTS ANIMAL; CREATE TABLE IF NOT EXISTS ANIMAL( ID BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(255), category_id bigint, foreign key (category_id) references CATEGORY(id) ); CREATE SEQUENCE HIBERNATE_SEQUENCE MINVALUE 1;
И data.sql:
insert into category(id, name) values (-3,'home'); insert into category(id, name) values (-2, 'wild'); insert into category(id, name) values (-1, 'bird'); insert into animal (id, name, category_id) values (1, 'cat', -3); insert into animal (id, name, category_id) values (2, 'dog', -3); insert into animal (id, name, category_id) values (3, 'eagle', -1); insert into animal (id, name, category_id) values (4, 'goat', -3); insert into animal (id, name, category_id) values (5, 'cow', -3); insert into animal (id, name, category_id) values (6, 'horse', -2);
Класс Animal:
@Data @NoArgsConstructor @AllArgsConstructor @Entity public class Animal { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; @ManyToOne private Category category; }
Класс Category:
@Data @NoArgsConstructor @AllArgsConstructor @Entity public class Category { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; }
AnimalRepository
AnimalRepository:
public interface AnimalRepository extends JpaRepository<Animal, Long>, CustomAnimalRepository { List<Animal> findAllByNameContaining(String str, Pageable pageable); Page<Animal> findAllByName(String name, Pageable pageable); @Query("select a from Animal a") Page<Animal> findAllAnimals(Pageable pageable); @Query(value = "select * from animal", nativeQuery = true) Page<Animal> findAllAnimalsNative(Pageable pageable); @Query(value = "select count(animal.id) as animalsCount, category.name as categoryName " + "from animal " + "join category on animal.category_id=category.id " + "group by category.name", nativeQuery = true) List<CountView> findAnimalsCountByCategoryNative(Pageable pageable); }
Как видно, во всех методах есть параметр Pageable — запрос страницы. При этом:
- Производные от имени запросы работают.
- Возвращать можно как List, так и Page.
- Работают @Query с JPQL.
- Работают нативные @Query.
- @Query могут быть сложные, с группировкой.
CountView — проекция (о проекциях тут):
public interface CountView { int getAnimalsCount(); String getCategoryName(); }
Теперь подробнее об аргументе Pageable.
Pagable
Для постраничного вывода элементов в параметр метода передается объект Pageable. Он содержит информацию о количестве элементов на странице и номере запрашиваемой страницы:
Pageable firstPageWithTwoElements = PageRequest.of(0, 2);
Страницы нумеруются с нуля. Выше показан запрос первой страницы, на каждой странице два элемента.
С сортировкой
В объекте Pageable можно задать заодно и сортировку. Так что элементы и сортируются, и выдаются постранично. Для этого используем третий параметр — Sort:
PageRequest.of(0, 2, Sort.by("name").descending());
Page
Page — объект, который помимо списка возвращаемых элементов, содержит общее число страниц, номер страницы и т.д.:
Чтобы вернуть сам список, мы вызываем метод getContent() полученного Page:
animals.getContent()
В GitHub есть примеры использования вышеприведенных методов репозитория — тесты на все методы AnimalRepository. В самом начале тестового класса — примеры использования базовых методов PagingAndSortingRepository (это суперкласс любого JpaRepository).
Теперь рассмотрим случай, когда запрос требуется написать с помощью EntityManager. Тут тоже можно сделать pagination.
Pagination и запрос с EntityManager
Создадим интерфейс CustomAnimalRepository и репозиторий CustomAnimalRepositoryImpl:
public class CustomAnimalRepositoryImpl implements CustomAnimalRepository { @PersistenceContext private EntityManager em; @Override public Page<Animal> getAnimals(Pageable pageable) { .... } }
Если посмотреть выше, CustomAnimalRepository уже включен в AnimalRepository:
public interface AnimalRepository extends JpaRepository<Animal, Long>, CustomAnimalRepository { .... }
Но в данной статье фокус на постраничном выводе.
Метод getAnimals() — это просто ручное воплощение постраничного вывода всех животных. Сама функциональность уже есть в PagingAndSortingRepository под именем:
Page<T> findAll(Pageable pageable);
Но наша цель — продемонстрировать использование EntityManager.
Итак, возвратить нужно PageImpl — реализацию Page. Для этого нужно получить сам список элементов на странице и общее число элементов. Поэтому запросов два.
Общее число animals:
Query queryCount = em.createQuery("Select count(id) From Animal a");
Список элементов на странице:
Query query = em.createQuery("select a from Animal a"); int pageNumber = pageable.getPageNumber(); int pageSize = pageable.getPageSize(); query.setFirstResult((pageNumber) * pageSize); query.setMaxResults(pageSize); List<Animal> animals = query.getResultList();
Весь метод getAnimals() в CustomAnimalRepositoryImpl:
public class CustomAnimalRepositoryImpl implements CustomAnimalRepository { @PersistenceContext private EntityManager em; @Override public Page<Animal> getAnimals(Pageable pageable) { Query query = em.createQuery("select a from Animal a"); int pageNumber = pageable.getPageNumber(); int pageSize = pageable.getPageSize(); query.setFirstResult((pageNumber) * pageSize); query.setMaxResults(pageSize); List<Animal> animals = query.getResultList(); Query queryCount = em.createQuery("Select count(id) From Animal a"); long count = (long) queryCount.getSingleResult(); return new PageImpl<Animal>(animals, pageable, count); } }
Таким образом можно постранично выводить любой сложный запрос.
Итоги
Весь пример с вызовом и тестированием всех методов доступен на GitHub.
Этот запрос точно работает как ожидается «@Query(value = «select count(animal.id) as animalsCount…», то есть, выводит постранично, а не весь найденный список? Просто с native sql нужно еще указывать третий параметр — countQuery. В этом случае pagination будет работать корректно
интересный вопрос. По документации вроде нужен countQuery, но у меня и так работает (тесты там есть).
У меня наоборот, заработало только с countQuery. Может быть связано с версиями.
Pageable firstPageWithTwoElements = PageRequest.of(0, 2);
Page request не возвращает Pageable а возвращает собственно PageRequest….
Верное замечание, спасибо.
Еще нужно учесть, что Spring Data под капотом пагинацию делает, формируя LIMIT/OFFSET запрос, у которого пропорционально значению OFFSET растет время исполнения запроса в БД, т.к. каждый последовательно пробегается весь индекс до смещения, а это очень дорого. На больших таблицах нормально работает, к сожалению, только вручную реализованная через кастомные репозитории keyset пагинация.