middle
Как реализовать пагинацию, фильтрацию и сортировку в REST API?
Пагинация, фильтрация и сортировка — обязательные механизмы для работы с большими коллекциями ресурсов, реализуемые через query-параметры HTTP-запросов.
Пагинация
| Подход | Пример | Когда использовать |
|---|---|---|
| Offset-based | GET /api/users?page=0&size=20 |
Наиболее распространённый, подходит для большинства случаев |
| Cursor-based | GET /api/users?cursor=eyJpZCI6NDJ9&size=20 |
Для больших объёмов данных, real-time лент |
Пример ответа с метаданными пагинации:
Пример
{
"content": [...],
"page": 0,
"size": 20,
"totalElements": 150,
"totalPages": 8,
"last": false,
"_links": {
"self": {"href": "/api/users?page=0&size=20"},
"next": {"href": "/api/users?page=1&size=20"},
"last": {"href": "/api/users?page=7&size=20"}
}
}
Фильтрация
Пример
GET /api/users?status=active
GET /api/users?status=active&role=admin
GET /api/users?age_gte=18&age_lte=65
GET /api/users?search=Иван
Сортировка
Пример
GET /api/users?sort=name,asc
GET /api/users?sort=createdAt,desc&sort=name,asc
Реализация в Spring (Spring Data)
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
// Пагинация и сортировка через Pageable
@GetMapping
public Page<User> getUsers(
@RequestParam(required = false) String status,
@RequestParam(required = false) String name,
@PageableDefault(size = 20, sort = "id") Pageable pageable) {
if (status != null) {
return userRepository.findByStatus(status, pageable);
}
if (name != null) {
return userRepository.findByNameContainingIgnoreCase(name, pageable);
}
return userRepository.findAll(pageable);
}
}
Запрос:
GET /api/users?status=active&page=0&size=10&sort=name,asc
Для сложной фильтрации можно использовать Spring Data Specifications:
@GetMapping
public Page<User> getUsers(
@RequestParam Map<String, String> filters,
Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (filters.containsKey("status")) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("status"), filters.get("status")));
}
if (filters.containsKey("name")) {
spec = spec.and((root, query, cb) ->
cb.like(cb.lower(root.get("name")),
"%" + filters.get("name").toLowerCase() + "%"));
}
return userRepository.findAll(spec, pageable);
}
На собеседовании: нужно знать оба подхода к пагинации (offset и cursor) и понимать, когда использовать какой. Частая ошибка — забыть про метаданные пагинации (totalElements, totalPages) в ответе или не упомянуть cursor-based подход.