← Назад к вопросам
Применял ли загрузку страницы контроллером в рабочей среде
2.0 Middle🔥 111 комментариев
#ORM и Hibernate#Spring Framework
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Pagination в Spring контроллерах: реальный опыт
Загрузка страницы (pagination) — критически важная техника при работе с большими датасетами. Я активно применял это в production-системах для оптимизации памяти и производительности.
Почему pagination необходима
Без pagination приложение может столкнуться с:
- OutOfMemoryError при загрузке миллионов записей в памяти
- Медленными запросами к БД (полное сканирование таблицы)
- Долгими ответами клиенту (тайм-ауты)
- Перегрузкой сети при передаче больших объёмов данных
Spring Data JPA Pagination
Самый удобный способ — использование Pageable интерфейса из Spring Data:
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@Autowired
private OrderRepository orderRepository;
// GET /api/v1/orders?page=0&size=20&sort=createdAt,desc
@GetMapping
public ResponseEntity<Page<OrderDTO>> getAllOrders(
@PageableDefault(size = 20, page = 0) Pageable pageable) {
Page<Order> page = orderRepository.findAll(pageable);
Page<OrderDTO> dtoPage = page.map(order ->
new OrderDTO(
order.getId(),
order.getNumber(),
order.getTotal(),
order.getStatus()
)
);
return ResponseEntity.ok(dtoPage);
}
// Пример с фильтром: GET /api/v1/orders?status=COMPLETED&page=0&size=20
@GetMapping("/by-status")
public Page<OrderDTO> getOrdersByStatus(
@RequestParam String status,
@PageableDefault(size = 20) Pageable pageable) {
return orderRepository
.findByStatus(OrderStatus.valueOf(status), pageable)
.map(order -> new OrderDTO(
order.getId(),
order.getNumber(),
order.getTotal(),
order.getStatus()
));
}
}
Repository слой
public interface OrderRepository extends JpaRepository<Order, Long> {
// JpaRepository наследует PagingAndSortingRepository
// автоматически поддерживает Pageable
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
Page<Order> findByCustomerId(Long customerId, Pageable pageable);
// Для сложных запросов
@Query("SELECT o FROM Order o WHERE o.total > :minAmount AND o.createdAt >= :dateFrom")
Page<Order> findHighValueOrders(
@Param("minAmount") BigDecimal minAmount,
@Param("dateFrom") LocalDateTime dateFrom,
Pageable pageable);
}
Оптимизация в production
1. Offset-based pagination
// Валидация максимального page и size
@GetMapping
public Page<OrderDTO> getAllOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
if (page < 0) page = 0;
if (size > 100) size = 100; // Максимум 100 на странице
if (page > 10000) {
throw new BadRequestException(
"Page number too large, use keyset pagination");
}
Pageable pageable = PageRequest.of(page, size);
return orderRepository.findAll(pageable)
.map(OrderDTO::from);
}
2. Keyset (cursor-based) pagination для больших наборов
public class KeysetPaginationRequest {
private Long lastId;
private int size = 20;
}
@GetMapping("/cursor")
public List<OrderDTO> getOrdersWithCursor(
@RequestParam(required = false) Long lastId,
@RequestParam(defaultValue = "20") int size) {
if (size > 100) size = 100;
List<Order> orders;
if (lastId == null) {
orders = orderRepository.findFirst20ByOrderByIdAsc();
} else {
orders = orderRepository
.findFirst21ByIdGreaterThanOrderByIdAsc(lastId)
.stream()
.skip(1)
.limit(size)
.collect(Collectors.toList());
}
return orders.stream()
.map(OrderDTO::from)
.collect(Collectors.toList());
}
Избегаем N+1 проблемы
// Решение с eager loading через JOIN FETCH
@GetMapping
public Page<OrderDTO> ordersOptimized(
@PageableDefault(size = 20) Pageable pageable) {
Page<Order> orders = orderRepository
.findAllWithCustomerAndItems(pageable);
return orders.map(OrderDTO::from);
}
// Repository
@Query("SELECT DISTINCT o FROM Order o LEFT JOIN FETCH o.customer c LEFT JOIN FETCH o.items i ORDER BY o.id DESC")
Page<Order> findAllWithCustomerAndItems(Pageable pageable);
Кэширование результатов
@GetMapping("/cached")
@Cacheable(value = "orders", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
public Page<OrderDTO> getOrdersCached(
@PageableDefault(size = 20) Pageable pageable) {
return orderRepository.findAll(pageable)
.map(OrderDTO::from);
}
Best Practices из production опыта
- Всегда ограничивай размер страницы — макс 100-200 элементов
- Используй Keyset pagination для очень больших датасетов (>10M записей)
- Добавляй индексы на поля для сортировки
- Избегай N+1 через JOIN FETCH в сложных запросах
- Кэшируй популярные данные (первые несколько страниц)
- Логируй медленные запросы — все что больше 100ms
- Тестируй с реальным объёмом данных в staging
Этот подход позволил обрабатывать миллионы записей эффективно и масштабируемо.