Pagination & Sorting
Introduction
When you have a large amount of data, returning it all at once is inefficient. Pagination splits the data into pages, and sorting orders the results as needed.
Basic Concepts
| Concept | Description |
|---|---|
| Page | Page number (starting from 0) |
| Size | Number of items per page |
| Sort | Ordering (ASC/DESC) |
| Total Elements | Total number of all items |
| Total Pages | Number of available pages |
Pageable & PageRequest
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public Page<Product> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id,asc") String[] sort) {
List<Sort.Order> orders = new ArrayList<>();
for (String s : sort) {
String[] parts = s.split(",");
orders.add(new Sort.Order(Sort.Direction.fromString(parts[1]), parts[0]));
}
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
return productService.findAll(pageable);
}
}
Request:
GET /api/products?page=0&size=10&sort=name,asc&sort=price,desc
Page Response
{
"content": [
{"id": 1, "name": "Laptop", "price": 15000000},
{"id": 2, "name": "Mouse", "price": 150000}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10
},
"totalElements": 50,
"totalPages": 5,
"number": 0,
"size": 10,
"first": true,
"last": false,
"empty": false
}
Service Layer
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Page<Product> findAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
public Page<Product> searchByName(String keyword, Pageable pageable) {
return productRepository.findByNameContaining(keyword, pageable);
}
}
Repository with Pagination
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findByNameContaining(String keyword, Pageable pageable);
Page<Product> findByPriceBetween(double min, double max, Pageable pageable);
}
Offset vs Cursor Pagination
| Type | How It Works | Pros | Cons |
|---|---|---|---|
| Offset | page=3&size=10 (skips 30) |
Simple, can jump to a page | Slow with very large data |
| Cursor | cursor=abc123 (last ID) |
Consistent when data changes | Cannot jump to a page |
Cursor-based (manual):
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByIdGreaterThanOrderByIdAsc(Long id, Pageable pageable);
}
Sorting
@GetMapping("/sorted")
public List<Product> getAllSorted(
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy);
return productRepository.findAll(sort);
}
Multiple fields:
Sort sort = Sort.by(
Sort.Order.desc("price"),
Sort.Order.asc("name")
);
return productRepository.findAll(sort);
Summary
PageRequest.of(page, size, sort)creates a Pageable object.Page<T>automatically includes metadata (totalElements, totalPages).- Offset is good for UIs, cursor is good for infinite scroll.
Next chapter: We'll cover Custom Query — how to write custom queries using JPQL and native SQL.