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]._
Вищезгадана схема добре працює для статичних пагінованих переглядів, коли клієнт хоче відображати фіксовану кількість записів на сторінці. Однак для нашого випадку основними недоліками цього підходу є:
- Вже встановлені 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]._
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