← Назад к вопросам

Применял ли загрузку страницы контроллером в рабочей среде

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 опыта

  1. Всегда ограничивай размер страницы — макс 100-200 элементов
  2. Используй Keyset pagination для очень больших датасетов (>10M записей)
  3. Добавляй индексы на поля для сортировки
  4. Избегай N+1 через JOIN FETCH в сложных запросах
  5. Кэшируй популярные данные (первые несколько страниц)
  6. Логируй медленные запросы — все что больше 100ms
  7. Тестируй с реальным объёмом данных в staging

Этот подход позволил обрабатывать миллионы записей эффективно и масштабируемо.

Применял ли загрузку страницы контроллером в рабочей среде | PrepBro