Пагінація на основі Limit/Offset з використанням Spring JPA

text
Під час міграції бізнес-логіки з застарілої монолітної системи на нову архітектуру мікросервісів, мені потрібно було реалізувати логіку пагінації, щоб забезпечити функціональну паритетність з існуючою системою. У моноліті пагінація здійснювалася за допомогою сирих SQL запитів до Postgres, таких як:

Select * from (subQuery) LIMIT ? OFFSET ?

У цій статті я розгляну стандартну модель PageRequest фреймворку Spring JPA, яка використовує формат page/size для отримання записів. Додатково я розгляну способи реалізації прогресивної пагінації за допомогою limit/offset, щоб досягти результатів, еквівалентних вищезгаданому SQL запиту.

Модель PageRequest

Розглянемо наступну модель сутності Student та клас Student Repository:

@Entity  
@Table(name = "students")  
@Data  
public class Student {   
 @Id  
 private Integer id;  
 @Column(name = "name")  
 private String name;  
 @Column(name = "grade")  
 private Integer grade;  
}
public interface StudentRepository extends JpaRepository {  
}

У нашому DAO-слої застосунку ми можемо отримати списки студентів з пагінацією наступним чином:

class StudentDataClient {  

 @Autowired  
 private StudentRepository studentRepository;  

 public List fetchList(int page, int size) {  
 Pageable pageable = PageRequest.of(page, size);  
 Page studentPage = studentRepository.findAll(pageable);  
 return studentPage.getContent();  
 }  
}

Клас PageRequest приймає два вхідні параметри: pageNumber та pageSize.

Якщо pageNumber = 2 і pageSize = 4, метод findAll(pageable) поверне другу сторінку (індексація з нуля) з 4 записами, що включають [student9, _student12]._

pic

Вищезгадана схема добре працює для статичних пагінованих переглядів, коли клієнт хоче відображати фіксовану кількість записів на сторінці. Однак для нашого випадку основними недоліками цього підходу є:

  • Вже встановлені API контракти з limit та offset як вхідними параметрами
  • Відхилення від поведінки старої системи, що вимагало б змін у клієнтському коді для обробки пагінованих результатів
  • Обмежена гнучкість клієнта щодо контролю за вибраними записами
  • Використання на фронтенді, наприклад, нескінченного прокручування, що потребує прогресивного завантаження результатів замість статичних відповідей на основі сторінок

Тому ми вирішили, що модель limit/offset буде більш чистим підходом для обробки пагінації, щоб клієнти зберігали тонкий контроль, при цьому забезпечуючи узгодженість з старою системою.

*Примітка: з точки зору продуктивності немає жодної різниці, оскільки Spring JPA внутрішньо обробляє PageRequest, використовуючи лише limit та offset.

Модель Limit/Offset

Тепер існує кілька способів реалізації пагінації на основі limit (кількість записів для отримання) /offset (початковий запис) з використанням Spring JPA. Я перелічу три основні підходи нижче.

1. Перетворення (offset, limit) → (page, size)

Можливо отримати (page, size) з (offset, limit) за допомогою простого перетворення, як показано нижче:

pageSize = limit
pageNumber = offset / limit

Отже, у наведеному прикладі, щоб отримати записи з student9_ до student12,_ offset = 8, а limit = 4, що відповідно переводиться у pageNumber = 2 та pageSize = 4.

Однак цей підхід не працює для offset, який не є кратним limit, оскільки Spring JPA не підтримує дробові pageNumber.

Наприклад, при offset = 9 та limit = 4 пагінація переведеться в pageNumber = 2.25. Це не дозволяє клієнту отримати записи, які починаються чи закінчуються в середині сторінки або охоплюють кілька сторінок, як наприклад [student10, _student13]._

pic

2.

text
Реалізація власного PageRequest

Створення власного класу, який реалізує інтерфейс Pageable та імплементує його методи: getOffset(), getPageNumber(), … тощо. Ця власна реалізація прийматиме limit та offset як параметри конструктора.

public class CustomPageRequest implements Pageable {  

 private int limit;  
 private int offset;  
 private final Sort sort;  

 public CustomPageRequest(int offset, int limit, Sort sort) {  
 this.limit = limit;  
 this.offset = offset;  
 this.sort = sort;  
 }  

 // Імплементація методів інтерфейсу Pageable  
}

Детальні обговорення можна знайти в цій гілці на Stack Overflow тут

3. Використання CriteriaQuery

Criteria Queries є програмним способом побудови типобезпечних і динамічних SQL запитів. Цей підхід є одним з найпростіших і найгнучкіших способів обробки складних запитів.

TypedQuery query = entityManager.createQuery(Student.class);  
query.setFirstResult(offset);  
query.setMaxResults(limit);  
List studentList = query.getResultList();

Детальніше про JPA Criteria Queries читайте тут

Висновки

Обидва підходи: власна реалізація PageRequest та CriteriaQuery є ефективними способами реалізації пагінації на основі limit/offset. Власна реалізація PageRequest забезпечує безшовну інтеграцію з методами репозиторіїв Spring Data JPA та підходить для стандартних випадків. З іншого боку, CriteriaQuery надає більшу гнучкість для створення динамічних та складних запитів, що робить його ідеальним для сценаріїв, які передбачають розширене фільтрування, сортування чи з’єднання кількох сутностей. Вибір між цими двома підходами залежить від конкретних вимог вашого застосунку, таких як складність логіки запиту та потреба в налаштуванні.

Перекладено з: Limit/Offset based Pagination with Spring JPA

Leave a Reply

Your email address will not be published. Required fields are marked *