Skip to main content

Custom Pagination with search and filters in Spring Boot

Every spring boot application is made to manage a large set of data. Also, we need to perform a search and filter the data according to need, And also we cannot load all data in one go on a single page so we need pagination too. In this article, we are going to demonstrate custom pagination with search and filter performed through ajax call.

Goal: This demonstration is performed on a set of students' data. We have written a method to generate sample data.

Spring Boot Pagination search and filter example

 

1. Initialize the project with the following dependencies

  1. Spring Web
  2. Thymeleaf
  3. Spring Data JPA
  4. MySql
  5. Lombok
  6. Spring Boot Dev Tools

2. Set the application properties

spring.datasource.url=jdbc:mysql://localhost:3306/paginationTutorial?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false

server.port=8889

 

3. Create the Student entity

@Getter
@Setter
@NoArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @Column(unique = true)
    private String email;
    private Klass klass;
    private LocalDate dateOfAdmission;

    public Student(String name, String email, Klass klass, LocalDate dateOfAdmission) {
        this.name=name;
        this.email=email;
        this.klass=klass;
        this.dateOfAdmission = dateOfAdmission;
    }
}

4. Enum to denote the class of student

@Getter
public enum  Klass {
     SIXTH("Sixth"),
     SEVENTH("Seventh"),
     EIGHTH("Eighth"),
     NINTH("Ninth"),
     TENTH("Tenth"),
     ELEVENTH("Eleventh"),
     TWELFTH("Twelfth");
    
    private String std;
    Klass(String std){this.std=std;}
}

 

5. Create JPA repository of entity

public interface StudentRepository extends JpaRepository<Student,Long> {
}

 

6. Create the search & filter command object (CO)

Command Objects will contain the request parameters. It is used to perform to request with page specifications.

@Getter
@Setter
public class StudentSearchCO {
    Optional<Integer> page;
    Optional<Integer> size;
    private Klass klass;
    private String dateOfAdmission;
    private String srchTxt;
}

 

7. Create a data transfer object (DTO) of the Entity for returning the response

DTOs are simple POJO classes that are sent with response entities.

@Getter
@Setter
@NoArgsConstructor
public class StudentDTO {
    private Long id;
    private String name;
    private String email;
    private String klass;
    private String dateOfAdmission;

    public StudentDTO(Student r) {
       this.id =r.getId();
       this.name=r.getName();
       this.email=r.getEmail();
       this.klass= r.getKlass().getStd();
       this.dateOfAdmission = Utils.convertDateToStr(r.getDateOfAdmission(),"dd/MM/yyyy");
    }
}

 

8. Create a service for implementing the business login

We will perform the filter on student name, class, and date of admission, and use CriteriaBuilder with the help of EntityManager.

@Service
public class StudentService {
    @Autowired
    private EntityManager entityManager;
    @Autowired
    private StudentRepository studentRepository;


    public Klass[] getKlasses(){
        return Klass.values();
    }
// it will return paged response
    public ResponseEntity getPaginatedStudents(StudentSearchCO srchCo) {
        try {
            CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            CriteriaQuery criteriaQuery = criteriaBuilder.createQuery();
            Root root = criteriaQuery.from(Student.class);
            List<Predicate> predicateList = new ArrayList<>();

            if (Strings.isNotBlank(srchCo.getSrchTxt())) {
                predicateList.add(criteriaBuilder.like(root.get("name"), "%" + srchCo.getSrchTxt() + "%"));
            }
            if (Strings.isNotBlank(srchCo.getDateOfAdmission())) {
                predicateList.add(criteriaBuilder.equal(root.get("dateOfAdmission"),Utils.convertStrToDate(srchCo.getDateOfAdmission(),"yyyy-MM-dd")));
            }
            if (srchCo.getKlass() != null) {
                predicateList.add(criteriaBuilder.equal(root.get("klass"), srchCo.getKlass()));
            }

            List<Order> orderList = new ArrayList<>();
                //ordered by id, you can write your own sorting with entity field names
                orderList.add(criteriaBuilder.asc(root.get("id")));

            criteriaQuery.orderBy(orderList);

            Predicate[] predicateArr = new Predicate[predicateList.size()];
            Predicate predicate = criteriaBuilder.and(predicateList.toArray(predicateArr));
            criteriaQuery = criteriaQuery.where(predicate);
            TypedQuery<Student> typedQuery = entityManager.createQuery(criteriaQuery.select(root));
            List<Student> resultList = typedQuery.getResultList(); // it may contain large data and some that we do not need too
            List<StudentDTO> dtos = new ArrayList<>(); // so we will convert in data transfer objects with required fields
            resultList.forEach(r -> {
                dtos.add(new StudentDTO(r));
            });
            int pageSize = srchCo.getSize().orElse(10);
            int pageNumber = srchCo.getPage().orElse(1) - 1;
            Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.Direction.ASC, "name");
            Page<StudentDTO> resultPage = null;

            if (resultList.size() > 0) {
                int from = pageNumber * pageSize;
                int to = from + pageSize;
                if (resultList.size() < to) {
                    to = resultList.size();
                }
                resultPage = new PageImpl<>(dtos.subList(from, to), pageable, dtos.size()); // list is sliced according to page number and size
            } else resultPage = new PageImpl<>(new ArrayList<>(), pageable, 0);

            return ResponseEntity.status(HttpStatus.CREATED).body(resultPage);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
        }

    }

// this method will be executed  just before services start serving the service.
    @PostConstruct
    public void booststrapSampleData(){
        if(studentRepository.count()==0) {
            List<Student> studentList = new ArrayList<>();
            List<Klass> klasses = Arrays.asList(getKlasses());
            List<String> names = Arrays.asList("Aman", "Sujeet", "Vipin kumar", "Lavkush Verma", "swapnil", "Sakshi", "Sanduana Siva");

            IntStream.range(0,klasses.size()).forEach(k -> {
                LocalDate date = LocalDate.now();
                IntStream.range(0,names.size()).forEach(n->{
                    studentList.add(new Student(names.get(n)+" "+k+n,names.get(n).toLowerCase()+k+n+"@demo.com",klasses.get(k),date.minusDays(n)));
                });
            });

            studentRepository.saveAll(studentList);
        }
    }
}

 

9. Create a controller

@Controller
public class StudentController {
    @Autowired
    private StudentService studentService;

// This method is to simply open the html page
    @RequestMapping(value={"/","/students"})
    public String students(){
        return "students";
    }

// This method will be called through ajax
    @PostMapping("/students")
    public ResponseEntity articles(StudentSearchCO srchCo){
        return studentService.getPaginatedStudents(srchCo);
    }
}

10. Create a utility class for date conversions

This conversion will be required to convert date format

public class Utils {
    public static LocalDate convertStrToDate(String dateStr, String dateFormat) {
        LocalDate date = null;
        if (dateStr != null) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
            date = LocalDate.parse(dateStr, dateTimeFormatter);
        }
        return date;
    }

    public static String convertDateToStr(LocalDate date, String dateFormat) {
        String dateStr = null;
        if (date != null) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateFormat);
            dateStr = date.format(dateTimeFormatter);
        }
        return dateStr;
    }
}

 

11. Create the HTML Data Table design

students.html

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>EasyTutorials - Pagination Example</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <hr>
    <h4 class="text-center text-primary">Custom Data Table with Search & filters</h4>
    <hr>
    <form id="filterForm" method="post" action="/students">
        <input type="hidden" id="pageInput"  name="page" value="1">

        <div class="row">
            <div class="col-md-3">
                <div class="pageControl">
                    <span>Show:</span>
                    <select style="width: 70px;"  id="pageSize" name="size" class="form-control">
                        <option value="10">10</option>
                        <option value="25">25</option>
                        <option value="50">50</option>
                    </select>
                    <span>entries</span>
                </div>
            </div>
            <div class="col-md-4">
                <div class="form-group">
                    <input type="text" class="form-control" id="srchTxt" name="srchTxt" placeholder="search with name">
                </div>
            </div>
            <div class="col-md-2">
                <div class="form-group">
                    <select class="form-control" id="klass" name="klass">
                        <option value="">--select class--</option>
                        <option th:each="c : ${@studentService.getKlasses()}" th:value="${c.name}"
                                th:text="${c.std}"></option>
                    </select>
                </div>
            </div>
            <div class="col-md-3">
                <div class="form-group">
                    <input type="date" class="form-control" id="dateOfAdmission" name="dateOfAdmission" placeholder="date of admission">
                </div>
            </div>

        </div>
    </form>
    <div class="loader lstloader"></div>
        <div class="table-responsive" style="overflow-y: auto">
            <table class="table table-hover table-bordered">
                <thead>
                <tr>
                    <th>Id</th>
                    <th>Name</th>
                    <th>Email</th>
                    <th>Class</th>
                    <th>Date of Admission</th>
                </tr>
                </thead>
                <tbody id="tbody"></tbody>
            </table>
        </div>
        <div class="row table-bottom">
            <div class="col-md-4 ">
                <div class="pageControl">
                    <span> Showing <strong id="offset">0</strong> to <strong  id="numberOfElements">0</strong> of <strong id="totalElements">0</strong> entries</span>
                </div>
            </div>
            <div class="col-md-4">
                <div class="pageControl ">
                    <span>Page No.</span>
                    <input type="text" id="gotoPage" class="goto form-control" onkeyup="changeJumpPage(this.value)"
                           value="1">
                    <button type="button" onclick="jumpToPage()" class="btn btn-primary">Jump</button>
                </div>
            </div>

            <div class="col-md-4">
                <div class="pageControl text-center ">
                    <button disabled="" class="btn btn-primary" id="prevBtn">Prev</button>
                    <div class="curr" id="currentPage">1/1</div>
                    <button disabled="" class="btn btn-primary" id="nextBtn">Next</button>
                </div>
            </div>
        </div>
</div>
</body>
</html>

 

12. Add CSS for data table design

You may add this to the style tag in the above Html

.pageControl{display: flex;flex-direction: row}
.curr{padding: 4px 10px;letter-spacing: 4px;}
.goto{width:70px}
.table td{padding:5px}
.form-control{height:30px;padding:0 10px}
.btn{padding:0 5px;height:30px}

.loader {height: 4px;
    width: 100%;
    position: relative;
    overflow: hidden;
    background-color: #ddd
}
.loader:before {
    display: block;
    position: absolute;
    content: "";
    left: -200px;
    width: 200px;
    height: 4px;
    background-color: #2980b9;
    animation: loading 1s linear infinite
}

@keyframes loading {
    from {left:-200px;width:30% }
    50%{width:30%}
    70%{width:70%}
    80%{left:50%}
    95%{left:120%}
    to{left:100%}
}

 

14. Add JavaScript code to handle AJAX calls

This will handle the data table events.

$(document).ready(function () {
    fetchData();
    $("#klass,#dateOfAdmission").on("change", function () {
        $("#pageInput").val(1);
        fetchData();
    });
    $("#srchTxt").on("input", function () {
        $("#pageInput").val(1);
        fetchData();
    });
});
var dataUrl = "/students";
var prevBtn = $("#prevBtn");
var nextBtn = $("#nextBtn");
var totalElements = $("#totalElements");
var numberOfElements = $("#numberOfElements");
var offset = $("#offset");
var currentPage = $("#currentPage");
var pageSize = $("#pageSize");
var pageNumber = $("#pageInput");
var from = 0;
var to = 0;

var totalPages=0;
var gotoPage = $("#gotoPage");
var loader = {};
loader.show=function () {
    $(".lstloader").show();
};
loader.hide=function () {
    $(".lstloader").hide();
};

function fetchData() {
    loader.show();
    var formData = getFormData($("#filterForm"));
    console.log(formData);
    $.post(dataUrl, formData,function (data,status) {
        console.log(data);
        setTableParams(data);

        var content = data.content;
        if (Object.keys(content).length > 0) {
            var tbody = $("<tbody/>", {id: "tbody"});
            var count = from;
            for (var i in content) {
                var rec = content[i];
                var tr = $("<tr>").append(
                    $("<td/>", {text:rec.id }),
                    $("<td/>", {text:rec.name}),
                    $("<td/>", {text:rec.email}),
                    $("<td/>", {text:rec.klass}),
                    $("<td/>", {text:rec.dateOfAdmission})
                );
                tbody.append(tr);
                count++;
            }
            $("#tbody").replaceWith(tbody);
        } else {
            $("#tbody").empty();
            $("#tbody").append(
                $("<tr/>").append($("<td/>",{
                    colspan:"20",
                    class:"text-center text-info",
                    text:"No records found"
                }))
            );
        }
        loader.hide();
    }).fail(function (t,e) {
        ajaxErr(t,e);
        loader.hide();
    });
}

function setTableParams(record) {
    pageSize.val(record.pageable.pageSize);
    totalElements.text(record.totalElements);
    if(record.totalElements=="0"){
        from = Number(record.pageable.offset);
    }else
        from = Number(record.pageable.offset) + 1;
    offset.text(from);
    to = (record.pageable.offset) + Number(record.numberOfElements);
    numberOfElements.text(to);
    pageNumber.val(Number(record.pageable.pageNumber) + 1);
    totalPages = Number(record.totalPages);
    currentPage.text(pageNumber.val()+"/"+totalPages);
    gotoPage.val(pageNumber.val());
    if (record.first == true)
        prevBtn.attr("disabled","");
    else
        prevBtn.removeAttr("disabled");
    if (record.last == true)
        nextBtn.attr("disabled","");
    else
        nextBtn.removeAttr("disabled");
}
prevBtn.click(function () {
    if (!prevBtn.hasClass("disabled")) {
        //console.log("Going to previous page.");
        pageNumber.val(Number(pageNumber.val()) - 1);
        fetchData();
    } else {
        console.log("Nothing to do");
    }
});

nextBtn.click(function () {
    if (!nextBtn.hasClass("disabled")) {
        //console.log("Going to next page.");
        pageNumber.val(Number(pageNumber.val()) + 1);
        fetchData();
    } else {
        console.log("Nothing to do");
    }
});

pageSize.on("change", function () {
    $("#pageInput").val(1);
    fetchData()
});

function jumpToPage() {
    var n = Number(gotoPage.val());
    //console.log(n);
    if(gotoPage.val()!=='' && n>0 && n<=totalPages){
        pageNumber.val(n);
        fetchData();
    }
}
// this will convert form data to json format
function getFormData(t) {
    var e = t.serializeArray(), n = {};
    return $.map(e, function (t, e) {
        n[t.name] = t.value
    }), n
}
// to filter out the response error
function ajaxErr(t, e) {
    var n = "";
    0 === t.status ? n = "Verify Network and refresh the page." : 404 == t.status ? n = "Requested page not found. [404]" : 500 == t.status ? n = "Internal Server Error [500]." : "parsererror" === e ? n = "Requested JSON parse failed." : "timeout" === e ? n = "Time out error." : "abort" === e ? n = "Ajax request aborted." : (console.log("uncaught error"), n = t.responseText),
        alert(n);
}

 

15. Screens

custom pagination and filter in spring boot

 

Thanks for reading this article, I hope you understand well the above. If you have any questions please comment below.

And as always you can find the source code on Github

 

Popular posts from this blog

Request Mapping Annotation in Spring Boot

The @RequestMapping is a class level  (also called type level) and method level annotation, it is used to process HTTP requests with specified URL patterns. It is used in and along with both @Controller and @RestController . Table of Contents Request Mapping Annotation in Spring Boot 1. How @RequestMapping annotation it is used? 2. Optional Elements of @RequestMapping 2.1 name, value and path 2.2 headers, consumes and produces 3. Specialization of @RequestMapping 1. How @RequestMapping annotation it is used? @Controller @RequestMapping("/student") public class StudentController{ @RequestMapping("/dashboard") public String dashboard(){ return "dashboard"; } @RequestMapping("result") public String result(){ return "result"; } } We can see in above code sample "/student" , "/dashboard" and "result" passed with annotation are called request value/path present in the URL ...

Maven or Gradle - built tool selection in Spring Boot

  Spring Boot -Selection of built tool Gradle Gradle is an open-source build automation tool that is designed to be flexible enough to build almost any type of software, It is fully open source and similar to Maven and Ant. But Gradle has taken advantage of both Maven and Ant and also it has removed the disadvantages of Maven and Ant and created as a first-class built tool. It uses domain-specific language based on the programming language Groovy , differentiating it from Apache Maven, which uses XML for its project configuration. Gradle allows to create or customize built procedure and we can create an additional task with groovy scripts that can be executed before/after built. It also determines the order of tasks run by using a directed acyclic graph . Several developers created Gradle and first released in 2007, and in 2013, it was adopted by Google as the build system for Android projects. It was designed to support multi-project builds that are expected...

Application Properties And Environment Profiling In Spring Boot

Application Properties As we know Spring Boot configures almost all the configurations automatically, it also enables us to customize the configurations and properties according to our needs and environment. There are various methods which we can use to do the same. We can either write all properties in a textual file, do programmatically in our Java classes, or can set it while starting the application through CLI by passing command-line arguments. By default, Spring Initializr creates an application.properties file inside the project's class path. But we can also define it in the YAML file. Know the differences between the .properties file and .yml file The properties that we are talking about are database credentials and URL, server port, logging file path, catch control variables, can write any custom string constants, etc. Table of Contents Application Properties  Method 1: Using application.properties  Method 2: Using application.yml file  Method 3...