Combined Search, Sorting and Pagination

This topic combines three features in one API: search narrows the records, sorting orders them, and pagination returns them a page at a time.It is used for real listing screens, such as an admin table with a search box, sortable columns and page buttons.Spring Data handles all three in one repository method, with a Pageable that carries the page number, page size and sort order.The client sends keyword, page, size and sort as URL parameters, and the database applies WHERE, ORDER BY and LIMIT together.

Features

  • One endpoint (GET /students) handles search, sort and paging together.
  • Every parameter is optional, so any combination works.
  • A department filter can be added on top of the keyword search.
  • The response includes the records plus totalElements, totalPages, hasNext, hasPrevious and sortedBy.
  • Sort fields are validated, and an invalid one returns 400 instead of a server error.
  • Page size is capped at 50 through application.properties.

Hands-on Experiment: Creating a spring boot application with Pagination, Sorting and Searching

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: listing
  • Name: listing
  • Package name: com.example.listing
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-listing-demo</artifactId>
    <version>1.0.0</version>
    <name>student-listing-demo</name>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-listing-demo
server.port=8082

spring.datasource.url=jdbc:mysql://localhost:3306/student_listing_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

# Page size limits
spring.data.web.pageable.default-page-size=5
spring.data.web.pageable.max-page-size=50Code language: PHP (php)

Step 5: Create Entity Class

package com.example.listing.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;

import java.time.LocalDate;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private String email;

    private String department;

    private Integer age;

    @Column(name = "registration_date")
    private LocalDate registrationDate;

    public Student() {
    }

    public Student(String name, String email, String department, Integer age, LocalDate registrationDate) {
        this.name = name;
        this.email = email;
        this.department = department;
        this.age = age;
        this.registrationDate = registrationDate;
    }

    @PrePersist
    public void setDefaultRegistrationDate() {
        if (registrationDate == null) {
            registrationDate = LocalDate.now();
        }
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public LocalDate getRegistrationDate() { return registrationDate; }
    public void setRegistrationDate(LocalDate registrationDate) { this.registrationDate = registrationDate; }
}

Step 6: Create below Repository Interface

package com.example.listing.repository;

import com.example.listing.entity.Student;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    // Search (keyword + optional department) + Sorting + Pagination in ONE query.
    // The Pageable carries both the page details and the sort order.
    // An empty keyword/department means "no filter".
    @Query("SELECT s FROM Student s WHERE "
            + "(LOWER(s.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR "
            + " LOWER(s.email) LIKE LOWER(CONCAT('%', :keyword, '%')) OR "
            + " LOWER(s.department) LIKE LOWER(CONCAT('%', :keyword, '%'))) AND "
            + "(:department = '' OR LOWER(s.department) = LOWER(:department))")
    Page<Student> searchStudents(@Param("keyword") String keyword,
                                 @Param("department") String department,
                                 Pageable pageable);
}

Step 7: Create below Service class

package com.example.listing.service;

import com.example.listing.entity.Student;
import com.example.listing.repository.StudentRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class StudentService {

    // Only these fields may be used for sorting
    private static final Set<String> ALLOWED_SORT_FIELDS =
            Set.of("id", "name", "email", "department", "age", "registrationDate");

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    public Student save(Student student) {
        return studentRepository.save(student);
    }

    public List<Student> saveAll(List<Student> students) {
        return studentRepository.saveAll(students);
    }

    public Map<String, Object> listStudents(String keyword, String department, Pageable pageable) {
        // 1. Validate sort fields
        pageable.getSort().forEach(order -> {
            if (!ALLOWED_SORT_FIELDS.contains(order.getProperty())) {
                throw new IllegalArgumentException("Cannot sort by '" + order.getProperty()
                        + "'. Allowed fields: " + ALLOWED_SORT_FIELDS);
            }
        });

        // 2. Search + sort + paginate
        Page<Student> page = studentRepository.searchStudents(clean(keyword), clean(department), pageable);

        // 3. Build a simple response
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("content", page.getContent());
        response.put("currentPage", page.getNumber());
        response.put("pageSize", page.getSize());
        response.put("totalElements", page.getTotalElements());
        response.put("totalPages", page.getTotalPages());
        response.put("hasNext", page.hasNext());
        response.put("hasPrevious", page.hasPrevious());
        response.put("sortedBy", page.getSort().isSorted()
                ? page.getSort().stream()
                        .map(o -> o.getProperty() + " " + o.getDirection())
                        .collect(Collectors.toList())
                : "unsorted");
        return response;
    }

    private String clean(String value) {
        return value == null ? "" : value.trim();
    }
}

Step 8: Create Controller class

package com.example.listing.controller;

import com.example.listing.entity.Student;
import com.example.listing.service.StudentService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    // POST /students
    @PostMapping
    public ResponseEntity<Student> create(@RequestBody Student student) {
        return ResponseEntity.status(HttpStatus.CREATED).body(studentService.save(student));
    }

    // POST /students/bulk
    @PostMapping("/bulk")
    public ResponseEntity<List<Student>> createBulk(@RequestBody List<Student> students) {
        return ResponseEntity.status(HttpStatus.CREATED).body(studentService.saveAll(students));
    }

    // GET /students?keyword=an&department=CSE&page=0&size=3&sort=age,desc
    // Every parameter is optional.
    @GetMapping
    public Map<String, Object> list(@RequestParam(required = false) String keyword,
                                    @RequestParam(required = false) String department,
                                    @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.ASC)
                                    Pageable pageable) {
        return studentService.listStudents(keyword, department, pageable);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, String>> handleBadRequest(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
    }
}

Step 9: Main Application

package com.example.listing;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StudentListingApplication {

    public static void main(String[] args) {
        SpringApplication.run(StudentListingApplication.class, args);
    }
}

Step 10: Run the StudentListingApplication

Scroll to Top