Pagination и Sorting в Spring Data JPA

В этой статье рассмотрим, как выводить данные постранично в 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 — объект, который помимо списка возвращаемых элементов, содержит общее число страниц, номер страницы и т.д.:

Page object
Page object

 

Чтобы вернуть сам список, мы вызываем метод 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 {
....
}
Если непонятно, как это работает, читайте про составные репозитории — есть такая возможность в Spring Data.

 

Но в данной статье фокус на постраничном выводе.

Метод 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.

Pagination и Sorting в Spring Data JPA: 6 комментариев

  1. Этот запрос точно работает как ожидается «@Query(value = «select count(animal.id) as animalsCount…», то есть, выводит постранично, а не весь найденный список? Просто с native sql нужно еще указывать третий параметр — countQuery. В этом случае pagination будет работать корректно

    1. интересный вопрос. По документации вроде нужен countQuery, но у меня и так работает (тесты там есть).

      1. У меня наоборот, заработало только с countQuery. Может быть связано с версиями.

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

    Page request не возвращает Pageable а возвращает собственно PageRequest….

  3. Еще нужно учесть, что Spring Data под капотом пагинацию делает, формируя LIMIT/OFFSET запрос, у которого пропорционально значению OFFSET растет время исполнения запроса в БД, т.к. каждый последовательно пробегается весь индекс до смещения, а это очень дорого. На больших таблицах нормально работает, к сожалению, только вручную реализованная через кастомные репозитории keyset пагинация.

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

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