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