Learnitweb

Pagination in JPA

What is Pagination?

Pagination is the process of dividing large data sets into smaller, manageable chunks, called pages. Instead of returning all rows from a database table (which can be thousands), you fetch a limited number of rows per request (like 10, 20, or 50).

Why Use Pagination?

  • Performance: Reduces memory usage and improves response time.
  • Scalability: Fetching 10 records is faster and lighter than fetching 10,000.
  • User Experience: UI can show data page-by-page, making it readable.

Pagination Support in Spring Data JPA

Spring Data JPA provides built-in support for pagination using:

  • Pageable interface
  • Page<T> and Slice<T> interfaces

Use Case: Pagination on a Product Entity

We’ll create a Product entity and implement an API to fetch products with pagination.

Repository

Spring Data JPA provides pagination-ready methods out of the box.

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
    Page<Product> findAll(Pageable pageable);
}

Service Layer

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public Page<Product> getProducts(int page, int size, String sortBy) {
        Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
        return productRepository.findAll(pageable);
    }
}

Controller Example

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping
    public Page<Product> getAllProducts(
            @RequestParam(defaultValue = "0") int page,      // Page number
            @RequestParam(defaultValue = "5") int size,      // Page size
            @RequestParam(defaultValue = "id") String sortBy // Sort field
    ) {
        return productService.getProducts(page, size, sortBy);
    }
}

Sample API Calls

URL: /api/products?page=0&size=10&sortBy=name

Returns:

{
  "content": [
    {
      "id": 1,
      "name": "Product 1",
      "price": 10.0
    },
    ...
  ],
  "totalPages": 10,
  "totalElements": 100,
  "last": false,
  "size": 10,
  "number": 0,
  "sort": {...},
  "first": true,
  "numberOfElements": 10,
  "empty": false
}

Understanding the Page<T> Object

Spring’s Page<T> object gives you:

  • getContent() – list of current page content
  • getTotalElements() – total number of records
  • getTotalPages() – total pages
  • getNumber() – current page number
  • isFirst() / isLast() – is it the first/last page
  • getSize() – size of each page

Pagination with Sorting

You can sort the result easily:

Pageable pageable = PageRequest.of(page, size, Sort.by("price").descending());

Or for multiple fields:

Sort sort = Sort.by("price").descending().and(Sort.by("name"));
Pageable pageable = PageRequest.of(page, size, sort);

Slice<T> in Spring Data JPA

Slice<T> is similar to Page<T>, but it doesn’t fetch the total number of elements or total pages.

When to use Slice<T> instead of Page<T>?

Use Slice<T> when:

  • You don’t care about the total number of items in the result.
  • You just want to fetch the next chunk/page of data.
  • You want better performance, especially with very large datasets, because Page<T> executes an additional COUNT(*) query to get totals, while Slice<T> skips that.
FeaturePage<T>Slice<T>
Total itemsYes (getTotalElements())No
Total pagesYes (getTotalPages())No
Has next/prev page?YesYes (hasNext(), hasPrevious())
Additional COUNT query?YesNo (faster)

How Does Slice<T> Work Internally?

To determine if more data is available, Slice<T> fetches one extra record than requested. So if you request size=5, it actually queries 6 records.

  • If it finds 6 records → it sets hasNext = true and drops the extra record.
  • If less than or equal to 5 records → it sets hasNext = false.

This allows efficient pagination without counting total rows.