Spring Boot Topic

Spring Boot Pagination

1. Introduction to Pagination

Pagination is a technique used to divide large datasets into smaller, manageable chunks (pages). It improves performance, reduces bandwidth usage, and enhances user experience.

Benefits of Pagination:

  • Performance: Faster response times with smaller datasets

  • Scalability: Handles large datasets efficiently

  • User Experience: Better navigation and loading times

  • Resource Management: Reduced memory and bandwidth usage

2. Spring Data JPA Pagination

2.1 Repository with Pagination Support

Spring Data JPA provides built-in support for pagination through the PagingAndSortingRepository interface.

Repository Interface:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // Basic pagination method
    Page<User> findAll(Pageable pageable);
    
    // Pagination with filtering
    Page<User> findByActiveTrue(Pageable pageable);
    
    Page<User> findByEmailContaining(String email, Pageable pageable);
    
    Page<User> findByUsernameContainingIgnoreCase(String username, Pageable pageable);
    
    Page<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end, Pageable pageable);
    
    // Pagination with joins
    @Query("SELECT u FROM User u JOIN u.roles r WHERE r.name = :roleName")
    Page<User> findByRoleName(@Param("roleName") String roleName, Pageable pageable);
    
    // Custom pagination with complex query
    @Query("SELECT u FROM User u WHERE " +
           "(:username IS NULL OR u.username LIKE %:username%) AND " +
           "(:email IS NULL OR u.email LIKE %:email%) AND " +
           "(:active IS NULL OR u.active = :active)")
    Page<User> findUsersByCriteria(
        @Param("username") String username,
        @Param("email") String email,
        @Param("active") Boolean active,
        Pageable pageable
    );
    
    // Count query for performance optimization
    @Query(value = "SELECT u FROM User u WHERE u.active = true",
           countQuery = "SELECT COUNT(u) FROM User u WHERE u.active = true")
    Page<User> findActiveUsersWithCount(Pageable pageable);
    
    // Native query with pagination
    @Query(value = "SELECT * FROM users u WHERE u.created_at >= :startDate",
           countQuery = "SELECT COUNT(*) FROM users u WHERE u.created_at >= :startDate",
           nativeQuery = true)
    Page<User> findUsersCreatedAfter(@Param("startDate") LocalDateTime startDate, Pageable pageable);
}

2.2 Service Layer Implementation

Service Class:

@Service
@Transactional(readOnly = true)
@Slf4j
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    // Basic pagination
    public Page<User> getAllUsers(Pageable pageable) {
        log.info("Fetching users with pagination: page {}, size {}", 
                 pageable.getPageNumber(), pageable.getPageSize());
        return userRepository.findAll(pageable);
    }
    
    // Pagination with filtering
    public Page<User> getActiveUsers(Pageable pageable) {
        return userRepository.findByActiveTrue(pageable);
    }
    
    public Page<User> searchUsersByEmail(String email, Pageable pageable) {
        return userRepository.findByEmailContaining(email, pageable);
    }
    
    public Page<User> searchUsersByUsername(String username, Pageable pageable) {
        return userRepository.findByUsernameContainingIgnoreCase(username, pageable);
    }
    
    // Advanced search with multiple criteria
    public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
        return userRepository.findUsersByCriteria(
            criteria.getUsername(),
            criteria.getEmail(),
            criteria.getActive(),
            pageable
        );
    }
    
    // Pagination with date range
    public Page<User> getUsersCreatedBetween(LocalDateTime start, LocalDateTime end, Pageable pageable) {
        return userRepository.findByCreatedAtBetween(start, end, pageable);
    }
    
    // Get users by role with pagination
    public Page<User> getUsersByRole(String roleName, Pageable pageable) {
        return userRepository.findByRoleName(roleName, pageable);
    }
    
    // Custom pagination with sorting
    public Page<User> getUsersWithCustomSorting(Pageable pageable) {
        // You can modify pageable or create custom sorting logic here
        return userRepository.findAll(pageable);
    }
    
    // Get page information without content (for metadata)
    public Page<User> getUsersMetadata(Pageable pageable) {
        Page<User> page = userRepository.findAll(pageable);
        
        // Log pagination information
        logPaginationInfo(page);
        
        return page;
    }
    
    private void logPaginationInfo(Page<User> page) {
        log.info("Page Number: {}", page.getNumber());
        log.info("Page Size: {}", page.getSize());
        log.info("Total Elements: {}", page.getTotalElements());
        log.info("Total Pages: {}", page.getTotalPages());
        log.info("Has Next: {}", page.hasNext());
        log.info("Has Previous: {}", page.hasPrevious());
        log.info("Is First: {}", page.isFirst());
        log.info("Is Last: {}", page.isLast());
    }
}

2.3 Custom Pagination Utility Class

Pagination Utility:

@Component
public class PaginationUtils {
    
    public static <T> Page<T> createPage(List<T> content, Pageable pageable, long total) {
        return new PageImpl<>(content, pageable, total);
    }
    
    public static Pageable createPageable(int page, int size, String sortBy, String direction) {
        Sort sort = direction.equalsIgnoreCase("desc") 
            ? Sort.by(sortBy).descending() 
            : Sort.by(sortBy).ascending();
        
        return PageRequest.of(page, size, sort);
    }
    
    public static Pageable createPageable(int page, int size, Sort sort) {
        return PageRequest.of(page, size, sort);
    }
    
    public static Pageable createPageable(int page, int size) {
        return PageRequest.of(page, size);
    }
    
    public static <T> PagedResponse<T> createPagedResponse(Page<T> page) {
        PagedResponse<T> response = new PagedResponse<>();
        response.setContent(page.getContent());
        response.setPageNumber(page.getNumber());
        response.setPageSize(page.getSize());
        response.setTotalElements(page.getTotalElements());
        response.setTotalPages(page.getTotalPages());
        response.setLast(page.isLast());
        response.setFirst(page.isFirst());
        response.setEmpty(page.isEmpty());
        response.setNumberOfElements(page.getNumberOfElements());
        
        // Add pagination links
        if (page.hasPrevious()) {
            response.setPreviousPage(page.getNumber() - 1);
        }
        if (page.hasNext()) {
            response.setNextPage(page.getNumber() + 1);
        }
        
        return response;
    }
    
    public static void validatePageable(Pageable pageable) {
        if (pageable.getPageNumber() < 0) {
            throw new IllegalArgumentException("Page number cannot be less than zero");
        }
        if (pageable.getPageSize() < 1) {
            throw new IllegalArgumentException("Page size must be at least 1");
        }
        if (pageable.getPageSize() > 100) {
            throw new IllegalArgumentException("Page size cannot be greater than 100");
        }
    }
}

3. Controller Implementation

3.1 REST Controller with Pagination

User Controller:

@RestController
@RequestMapping("/api/users")
@Validated
@Slf4j
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // Get all users with pagination
    @GetMapping
    public ResponseEntity<PagedResponse<User>> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String direction) {
        
        Pageable pageable = PaginationUtils.createPageable(page, size, sortBy, direction);
        PaginationUtils.validatePageable(pageable);
        
        Page<User> userPage = userService.getAllUsers(pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Search users with pagination
    @GetMapping("/search")
    public ResponseEntity<PagedResponse<User>> searchUsers(
            @RequestParam(required = false) String username,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Boolean active,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "username") String sortBy,
            @RequestParam(defaultValue = "asc") String direction) {
        
        Pageable pageable = PaginationUtils.createPageable(page, size, sortBy, direction);
        
        UserSearchCriteria criteria = new UserSearchCriteria(username, email, active);
        Page<User> userPage = userService.searchUsers(criteria, pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Get active users with pagination
    @GetMapping("/active")
    public ResponseEntity<PagedResponse<User>> getActiveUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<User> userPage = userService.getActiveUsers(pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Get users by email containing
    @GetMapping("/search/email")
    public ResponseEntity<PagedResponse<User>> searchUsersByEmail(
            @RequestParam String email,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<User> userPage = userService.searchUsersByEmail(email, pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Get users by role with pagination
    @GetMapping("/role/{roleName}")
    public ResponseEntity<PagedResponse<User>> getUsersByRole(
            @PathVariable String roleName,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<User> userPage = userService.getUsersByRole(roleName, pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Advanced search with multiple sorting
    @GetMapping("/advanced-search")
    public ResponseEntity<PagedResponse<User>> advancedSearch(
            @RequestParam(required = false) String username,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Boolean active,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "username,asc") String[] sort) {
        
        // Parse multiple sort parameters
        Sort sorting = parseSortParameters(sort);
        Pageable pageable = PageRequest.of(page, size, sorting);
        
        UserSearchCriteria criteria = new UserSearchCriteria(username, email, active);
        Page<User> userPage = userService.searchUsers(criteria, pageable);
        PagedResponse<User> response = PaginationUtils.createPagedResponse(userPage);
        
        return ResponseEntity.ok(response);
    }
    
    // Custom method to parse multiple sort parameters
    private Sort parseSortParameters(String[] sort) {
        if (sort == null || sort.length == 0) {
            return Sort.by("id").ascending();
        }
        
        List<Sort.Order> orders = new ArrayList<>();
        for (String sortParam : sort) {
            String[] sortParts = sortParam.split(",");
            if (sortParts.length == 2) {
                String property = sortParts[0];
                String direction = sortParts[1];
                Sort.Order order = direction.equalsIgnoreCase("desc") 
                    ? Sort.Order.desc(property) 
                    : Sort.Order.asc(property);
                orders.add(order);
            }
        }
        
        return Sort.by(orders);
    }
}

3.2 Response DTO Classes

PagedResponse DTO:

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PagedResponse<T> {
    private List<T> content;
    private int pageNumber;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private boolean last;
    private boolean first;
    private boolean empty;
    private int numberOfElements;
    
    // Navigation properties
    private Integer previousPage;
    private Integer nextPage;
    
    // Metadata
    private Map<String, Object> metadata;
    
    public PagedResponse(List<T> content, int pageNumber, int pageSize, 
                        long totalElements, int totalPages) {
        this.content = content;
        this.pageNumber = pageNumber;
        this.pageSize = pageSize;
        this.totalElements = totalElements;
        this.totalPages = totalPages;
        this.last = (pageNumber + 1) >= totalPages;
        this.first = pageNumber == 0;
        this.empty = content == null || content.isEmpty();
        this.numberOfElements = content != null ? content.size() : 0;
        
        // Set navigation
        if (!first) {
            this.previousPage = pageNumber - 1;
        }
        if (!last) {
            this.nextPage = pageNumber + 1;
        }
    }
    
    public void addMetadata(String key, Object value) {
        if (this.metadata == null) {
            this.metadata = new HashMap<>();
        }
        this.metadata.put(key, value);
    }
}

Search Criteria DTO:

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserSearchCriteria {
    private String username;
    private String email;
    private Boolean active;
    private LocalDateTime createdAfter;
    private LocalDateTime createdBefore;
    
    public UserSearchCriteria(String username, String email, Boolean active) {
        this.username = username;
        this.email = email;
        this.active = active;
    }
    
    public boolean hasFilters() {
        return username != null || email != null || active != null || 
               createdAfter != null || createdBefore != null;
    }
}

4. Advanced Pagination Features

4.1 Custom Pagination with Specifications

Specification-based Pagination:

@Service
@Transactional(readOnly = true)
public class UserSpecificationService {
    
    @Autowired
    private UserRepository userRepository;
    
    public Page<User> findUsersWithSpecification(UserSearchCriteria criteria, Pageable pageable) {
        Specification<User> spec = buildSpecification(criteria);
        return userRepository.findAll(spec, pageable);
    }
    
    private Specification<User> buildSpecification(UserSearchCriteria criteria) {
        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            if (StringUtils.hasText(criteria.getUsername())) {
                predicates.add(criteriaBuilder.like(
                    criteriaBuilder.lower(root.get("username")),
                    "%" + criteria.getUsername().toLowerCase() + "%"
                ));
            }
            
            if (StringUtils.hasText(criteria.getEmail())) {
                predicates.add(criteriaBuilder.like(
                    criteriaBuilder.lower(root.get("email")),
                    "%" + criteria.getEmail().toLowerCase() + "%"
                ));
            }
            
            if (criteria.getActive() != null) {
                predicates.add(criteriaBuilder.equal(root.get("active"), criteria.getActive()));
            }
            
            if (criteria.getCreatedAfter() != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(
                    root.get("createdAt"), criteria.getCreatedAfter()
                ));
            }
            
            if (criteria.getCreatedBefore() != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(
                    root.get("createdAt"), criteria.getCreatedBefore()
                ));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

4.2 Pagination with Projections

Interface-based Projections:

public interface UserProjection {
    Long getId();
    String getUsername();
    String getEmail();
    boolean isActive();
    
    @Value("#{target.username + ' (' + target.email + ')'}")
    String getDisplayName();
}

// Repository methods with projections
public interface UserRepository extends JpaRepository<User, Long> {
    
    <T> Page<T> findByActiveTrue(Pageable pageable, Class<T> type);
    
    @Query("SELECT u.id as id, u.username as username, u.email as email, u.active as active FROM User u")
    Page<UserProjection> findAllProjectedBy(Pageable pageable);
    
    Page<UserSummary> findByEmailContaining(String email, Pageable pageable);
}

// Custom projection interface
public interface UserSummary {
    String getUsername();
    String getEmail();
    Long getPostCount();
    
    default String getInfo() {
        return getUsername() + " - " + getEmail() + " (" + getPostCount() + " posts)";
    }
}

// Custom query with projection
@Query("SELECT u.username as username, u.email as email, COUNT(p) as postCount " +
       "FROM User u LEFT JOIN u.posts p GROUP BY u.id")
Page<UserSummary> findUserSummaries(Pageable pageable);

4.3 Pagination with Native Queries

Native Query Pagination:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query(value = """
        SELECT u.*, 
               (SELECT COUNT(*) FROM posts p WHERE p.user_id = u.id) as post_count
        FROM users u 
        WHERE u.active = true
        AND (:username IS NULL OR u.username LIKE CONCAT('%', :username, '%'))
        ORDER BY u.created_at DESC
        """,
        countQuery = """
        SELECT COUNT(*) FROM users u 
        WHERE u.active = true
        AND (:username IS NULL OR u.username LIKE CONCAT('%', :username, '%'))
        """,
        nativeQuery = true)
    Page<Object[]> findActiveUsersWithPostCount(
        @Param("username") String username,
        Pageable pageable
    );
    
    // Using EntityManager for complex native queries
    @PersistenceContext
    private EntityManager entityManager;
    
    public Page<User> findComplexNativeQuery(Pageable pageable) {
        String sql = "SELECT * FROM users u WHERE u.active = true";
        String countSql = "SELECT COUNT(*) FROM users u WHERE u.active = true";
        
        Query query = entityManager.createNativeQuery(sql, User.class);
        Query countQuery = entityManager.createNativeQuery(countSql);
        
        // Apply pagination
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(pageable.getPageSize());
        
        List<User> content = query.getResultList();
        Long total = ((Number) countQuery.getSingleResult()).longValue();
        
        return new PageImpl<>(content, pageable, total);
    }
}

5. Global Pagination Configuration

5.1 Custom Pageable Handler

Pageable Configuration:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver();
        resolver.setOneIndexedParameters(true); // Use 1-based page indexing
        resolver.setMaxPageSize(100); // Maximum page size
        resolver.setFallbackPageable(PageRequest.of(0, 20)); // Default pagination
        argumentResolvers.add(resolver);
    }
}

5.2 Custom Page Serializer

Custom JSON Serializer for Page:

@Component
public class PageSerializer extends JsonSerializer<Page<?>> {
    
    @Override
    public void serialize(Page<?> page, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        
        gen.writeStartObject();
        
        // Content
        gen.writeObjectField("content", page.getContent());
        
        // Pagination info
        gen.writeNumberField("pageNumber", page.getNumber());
        gen.writeNumberField("pageSize", page.getSize());
        gen.writeNumberField("totalElements", page.getTotalElements());
        gen.writeNumberField("totalPages", page.getTotalPages());
        
        // Navigation
        gen.writeBooleanField("first", page.isFirst());
        gen.writeBooleanField("last", page.isLast());
        gen.writeBooleanField("empty", page.isEmpty());
        gen.writeNumberField("numberOfElements", page.getNumberOfElements());
        
        // Links
        if (page.hasPrevious()) {
            gen.writeNumberField("previousPage", page.getNumber() - 1);
        }
        if (page.hasNext()) {
            gen.writeNumberField("nextPage", page.getNumber() + 1);
        }
        
        gen.writeEndObject();
    }
}

6. Testing Pagination

6.1 Repository Tests

Pagination Repository Test:

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = {
    "spring.jpa.show-sql=true",
    "spring.jpa.properties.hibernate.format_sql=true"
})
class UserRepositoryPaginationTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        // Create test data
        for (int i = 1; i <= 25; i++) {
            User user = new User("user" + i, "user" + i + "@example.com");
            user.setActive(i % 2 == 0); // Every second user is active
            entityManager.persist(user);
        }
        entityManager.flush();
    }
    
    @Test
    void whenFindAllWithPagination_thenReturnFirstPage() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        
        // When
        Page<User> result = userRepository.findAll(pageable);
        
        // Then
        assertThat(result.getContent()).hasSize(10);
        assertThat(result.getNumber()).isEqualTo(0);
        assertThat(result.getTotalElements()).isEqualTo(25);
        assertThat(result.getTotalPages()).isEqualTo(3);
        assertThat(result.isFirst()).isTrue();
        assertThat(result.hasNext()).isTrue();
    }
    
    @Test
    void whenFindAllWithPagination_thenReturnSecondPage() {
        // Given
        Pageable pageable = PageRequest.of(1, 10);
        
        // When
        Page<User> result = userRepository.findAll(pageable);
        
        // Then
        assertThat(result.getContent()).hasSize(10);
        assertThat(result.getNumber()).isEqualTo(1);
        assertThat(result.isFirst()).isFalse();
        assertThat(result.hasNext()).isTrue();
    }
    
    @Test
    void whenFindActiveUsersWithPagination_thenReturnOnlyActiveUsers() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        
        // When
        Page<User> result = userRepository.findByActiveTrue(pageable);
        
        // Then
        assertThat(result.getContent()).allMatch(User::isActive);
        assertThat(result.getTotalElements()).isEqualTo(12); // 12 active users out of 25
    }
    
    @Test
    void whenSearchUsersWithPagination_thenReturnFilteredResults() {
        // Given
        Pageable pageable = PageRequest.of(0, 5);
        
        // When
        Page<User> result = userRepository.findByUsernameContainingIgnoreCase("user1", pageable);
        
        // Then
        assertThat(result.getContent()).hasSize(5);
        assertThat(result.getContent())
            .extracting(User::getUsername)
            .allMatch(username -> username.contains("user1"));
    }
}

6.2 Service Tests

Pagination Service Test:

@ExtendWith(MockitoExtension.class)
class UserServicePaginationTest {
    
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void whenGetAllUsersWithPagination_thenReturnPagedResult() {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        List<User> users = createMockUsers(10);
        Page<User> expectedPage = new PageImpl<>(users, pageable, 25);
        
        when(userRepository.findAll(pageable)).thenReturn(expectedPage);
        
        // When
        Page<User> result = userService.getAllUsers(pageable);
        
        // Then
        assertThat(result).isEqualTo(expectedPage);
        assertThat(result.getContent()).hasSize(10);
        assertThat(result.getTotalElements()).isEqualTo(25);
        
        verify(userRepository).findAll(pageable);
    }
    
    @Test
    void whenSearchUsersWithCriteria_thenReturnFilteredPagedResult() {
        // Given
        Pageable pageable = PageRequest.of(0, 5);
        UserSearchCriteria criteria = new UserSearchCriteria("john", null, true);
        List<User> users = createMockUsers(5);
        Page<User> expectedPage = new PageImpl<>(users, pageable, 15);
        
        when(userRepository.findUsersByCriteria("john", null, true, pageable))
            .thenReturn(expectedPage);
        
        // When
        Page<User> result = userService.searchUsers(criteria, pageable);
        
        // Then
        assertThat(result.getContent()).hasSize(5);
        assertThat(result.getTotalElements()).isEqualTo(15);
        
        verify(userRepository).findUsersByCriteria("john", null, true, pageable);
    }
    
    private List<User> createMockUsers(int count) {
        List<User> users = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            users.add(new User((long) i, "user" + i, "user" + i + "@example.com"));
        }
        return users;
    }
}

6.3 Controller Tests

Pagination Controller Test:

@WebMvcTest(UserController.class)
@Import({UserService.class, PaginationUtils.class})
class UserControllerPaginationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserRepository userRepository;
    
    @Test
    void whenGetAllUsers_thenReturnPagedResponse() throws Exception {
        // Given
        Pageable pageable = PageRequest.of(0, 10);
        List<User> users = createMockUsers(10);
        Page<User> userPage = new PageImpl<>(users, pageable, 25);
        
        when(userRepository.findAll(pageable)).thenReturn(userPage);
        
        // When & Then
        mockMvc.perform(get("/api/users")
                .param("page", "0")
                .param("size", "10")
                .param("sortBy", "username")
                .param("direction", "asc"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").isArray())
                .andExpect(jsonPath("$.content.length()").value(10))
                .andExpect(jsonPath("$.pageNumber").value(0))
                .andExpect(jsonPath("$.pageSize").value(10))
                .andExpect(jsonPath("$.totalElements").value(25))
                .andExpect(jsonPath("$.totalPages").value(3))
                .andExpect(jsonPath("$.first").value(true))
                .andExpect(jsonPath("$.last").value(false));
    }
    
    @Test
    void whenSearchUsers_thenReturnFilteredPagedResponse() throws Exception {
        // Given
        Pageable pageable = PageRequest.of(0, 5);
        List<User> users = createMockUsers(5);
        Page<User> userPage = new PageImpl<>(users, pageable, 15);
        
        when(userRepository.findUsersByCriteria("john", "example.com", true, pageable))
            .thenReturn(userPage);
        
        // When & Then
        mockMvc.perform(get("/api/users/search")
                .param("username", "john")
                .param("email", "example.com")
                .param("active", "true")
                .param("page", "0")
                .param("size", "5"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content.length()").value(5))
                .andExpect(jsonPath("$.totalElements").value(15));
    }
    
    private List<User> createMockUsers(int count) {
        List<User> users = new ArrayList<>();
        for (int i = 1; i <= count; i++) {
            users.add(new User((long) i, "user" + i, "user" + i + "@example.com"));
        }
        return users;
    }
}

7. Performance Optimization

7.1 Pagination Best Practices

Configuration Properties:

# application.properties
spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=100
spring.data.web.pageable.one-indexed-parameters=false

# JPA properties for pagination optimization
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?