Spring Boot + Spring Data JPA + Thymeleaf ,Pagination & CRUD Application Example

Hello everyone, hope you are doing well. Today we will learn how to develop a simple Pagination & CRUD Application using Spring Boot, Spring Data JPA, and Thymeleaf.


Technologies Used:

  • Java 17
  • Spring Boot 2.5.5
  • Spring Data JPA
  • Thymeleaf
  • H2 Database
  • HTML
  • CSS
  • Bootstrap
  • jQuery

A little bit of Background

Thymeleaf

Thymeleaf is a modern server-side Java template engine for both web and standalone environments. 
Thymeleaf's main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams. 
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.

Spring Data JPA

Spring Data JPA provides repository support for the Java Persistence API (JPA). It eases development of applications that need to access JPA data sources.


Spring Boot

Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can "just run". 


Let's begin,

Final Project Directory



Pom.xml

A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains information about the project and configuration details utilized by Maven to build the project. 
<?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
http://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>2.5.5</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-thymeleaf-crud-pagination</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-thymeleaf-crud-pagination</name>

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

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

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



Create an User Entity

The @Entity annotation specifies that the class is an entity and is mapped to a database table. 
The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys. 
The @Column annotation is used to specify the mapped column for a persistent property or field. If no Column annotation is specified, the default value will be applied.
package com.knf.dev.demo.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "_user")
public class User {

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

@Column(name = "firstName")
private String firstName;

@Column(name = "lastName")
private String lastName;

@Column(name = "email", nullable = false, length = 200)
private String email;

public User() {

}

public Long getId() {
return id;
}

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

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

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

public User(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
}



Create User Repository

JpaRepository is a JPA (Java Persistence API) specific extension of Repository. It contains the full API of CrudRepository and PagingAndSortingRepository. So it contains API for basic CRUD operations and also API for pagination and sorting.
package com.knf.dev.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.knf.dev.demo.domain.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

}



Create User Service

package com.knf.dev.demo.service;

import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.knf.dev.demo.domain.User;
import com.knf.dev.demo.exception.RecordNotFoundException;

public interface UserService {

Page<User> findAllPageable(Pageable pageable);

public void deleteUserById(Long id) throws RecordNotFoundException;

public User createOrUpdateUser(User entity);

public User getUserById(Long id) throws RecordNotFoundException;

public List<User> getAllusers();
}



Create User Service Implementation

package com.knf.dev.demo.service.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.knf.dev.demo.domain.User;
import com.knf.dev.demo.exception.RecordNotFoundException;
import com.knf.dev.demo.repository.UserRepository;
import com.knf.dev.demo.service.UserService;

@Service
public class UserServiceImpl implements UserService {

private final UserRepository userRepository;

public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public Page<User> findAllPageable(Pageable pageable) {
return userRepository.findAll(pageable);
}

@Override
public void deleteUserById(Long id) throws RecordNotFoundException {

Optional<User> user = userRepository.findById(id);

if (user.isPresent()) {
userRepository.deleteById(id);
} else {
throw new RecordNotFoundException("No user record exist for given id");
}
}

@Override
public User createOrUpdateUser(User entity) {
if (entity.getId() == null) {
entity = userRepository.save(entity);

return entity;
} else {
Optional<User> user = userRepository.findById(entity.getId());

if (user.isPresent()) {
User newEntity = user.get();
newEntity.setEmail(entity.getEmail());
newEntity.setFirstName(entity.getFirstName());
newEntity.setLastName(entity.getLastName());

newEntity = userRepository.save(newEntity);

return newEntity;
} else {
entity = userRepository.save(entity);

return entity;
}
}
}

@Override
public User getUserById(Long id) throws RecordNotFoundException {

Optional<User> user = userRepository.findById(id);

if (user.isPresent()) {
return user.get();
} else {
throw new RecordNotFoundException("No user record exist for given id");
}
}

@Override
public List<User> getAllusers() {

List<User> result = userRepository.findAll();
if (result.size() > 0) {
return result;
} else {
return new ArrayList<User>();
}
}
}



Create a Pager

package com.knf.dev.demo.util;

public class Pager {

private int buttonsToShow = 3;

private int startPage;

private int endPage;

public Pager(int totalPages, int currentPage, int buttonsToShow) {

setButtonsToShow(buttonsToShow);

int halfPagesToShow = getButtonsToShow() / 2;

if (totalPages <= getButtonsToShow()) {
setStartPage(1);
setEndPage(totalPages);

} else if (currentPage - halfPagesToShow <= 0) {
setStartPage(1);
setEndPage(getButtonsToShow());

} else if (currentPage + halfPagesToShow == totalPages) {
setStartPage(currentPage - halfPagesToShow);
setEndPage(totalPages);

} else if (currentPage + halfPagesToShow > totalPages) {
setStartPage(totalPages - getButtonsToShow() + 1);
setEndPage(totalPages);

} else {
setStartPage(currentPage - halfPagesToShow);
setEndPage(currentPage + halfPagesToShow);
}

}

public int getButtonsToShow() {
return buttonsToShow;
}

public void setButtonsToShow(int buttonsToShow) {
if (buttonsToShow % 2 != 0) {
this.buttonsToShow = buttonsToShow;
} else {
throw new IllegalArgumentException("Must be an odd value!");
}
}

public int getStartPage() {
return startPage;
}

public void setStartPage(int startPage) {
this.startPage = startPage;
}

public int getEndPage() {
return endPage;
}

public void setEndPage(int endPage) {
this.endPage = endPage;
}

@Override
public String toString() {
return "Pager [startPage=" + startPage + ","
+ " endPage=" + endPage + "]";
}

}



Create RecordNotFoundException

package com.knf.dev.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class RecordNotFoundException extends Exception {

private static final long serialVersionUID = 1L;

public RecordNotFoundException(String message) {
super(message);
}

public RecordNotFoundException
(String message, Throwable throwable) {
super(message, throwable);
}
}



Create User Controller

package com.knf.dev.demo.controller;

import java.util.Optional;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import com.knf.dev.demo.domain.User;
import com.knf.dev.demo.exception.RecordNotFoundException;
import com.knf.dev.demo.service.UserService;
import com.knf.dev.demo.util.Pager;

@Controller
public class UserController {

private static final int BUTTONS_TO_SHOW = 3;
private static final int INITIAL_PAGE = 0;
private static final int INITIAL_PAGE_SIZE = 3;
private static final int[] PAGE_SIZES = { 3, 6, 9, 12 };

private final UserService service;

public UserController(UserService service) {
this.service = service;
}

@GetMapping("/")
public ModelAndView showPersonsPage(@RequestParam("pageSize")
Optional<Integer> pageSize,
@RequestParam("page") Optional<Integer> page) {

var modelAndView = new ModelAndView("list-users");

int evalPageSize = pageSize.orElse(INITIAL_PAGE_SIZE);

int evalPage = page.filter(p -> p >= 1).map(p -> p - 1)
.orElse(INITIAL_PAGE);

var users = service.findAllPageable
(PageRequest.of(evalPage, evalPageSize));
var pager = new Pager(users.getTotalPages(),
users.getNumber(), BUTTONS_TO_SHOW);

modelAndView.addObject("users", users);
modelAndView.addObject("selectedPageSize", evalPageSize);
modelAndView.addObject("pageSizes", PAGE_SIZES);
modelAndView.addObject("pager", pager);
return modelAndView;
}

@GetMapping(path = { "/addOrEdit", "/addOrEdit/{id}" })
public String editUserById(Model model, @PathVariable("id")
Optional<Long> id) throws RecordNotFoundException {

if (id.isPresent()) {
User entity = service.getUserById(id.get());
model.addAttribute("user", entity);
} else {
model.addAttribute("user", new User());
}
return "add-edit-user";
}

@GetMapping(path = "/delete/{id}")
public String deleteUserById(Model model,
@PathVariable("id") Long id)
throws RecordNotFoundException {

service.deleteUserById(id);
return "redirect:/";
}

@PostMapping(path = "/createOrUpdateUser")
public String createOrUpdateUser(User user) {

service.createOrUpdateUser(user);
return "redirect:/";
}
}



add-edit-user.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Add User</title>
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.4.1/css/all.css">
<link rel="stylesheet"
th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}" />
<link th:href="@{/css/style.css}" rel="stylesheet" />

</head>

<body>
<div class="container my-5">
<h3 th:if="${user.id != null}">Edit User</h3>
<h3 th:if="${user.id == null}">Add User</h3>
<div class="card">
<div class="card-body">
<div class="col-md-10">
<form action="#" th:action="@{/createOrUpdateUser}"
th:object="${user}" method="post">
<div class="row">
<div class="form-group col-md-8">
<label for="name" class="col-form-label"> First Name </label> <input
type="text" th:field="*{firstName}" class="form-control"
id="firstName" placeholder="First Name" />
</div>

<div class="form-group col-md-8">
<label for="name" class="col-form-label"> Last Name </label> <input
type="text" th:field="*{lastName}" class="form-control"
id="lastName" placeholder="Last Name" />
</div>

<div class="form-group col-md-8">
<label for="email" class="col-form-label"> Email </label> <input
type="text" th:field="*{email}" class="form-control" id="email"
placeholder="Email Id" />
</div>

<div class="col-md-6">
<input type="submit" class="btn btn-success" value=" Submit ">
</div>

<input type="hidden" id="id" th:field="*{id}">
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>



list-users.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>All Users</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.4.1/css/all.css">
<link rel="stylesheet"
th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}" />

<style>
.pagination-container {
text-align: right;
padding-right: 0;
}

.page-select {
padding-left: 0;
}

.disabled {
pointer-events: none;
opacity: 0.5;
}

.pointer-disabled {
pointer-events: none;
}
</style>
</head>

<body>
<div class="container">

<div th:switch="${users}" class="container my-5">
<p class="my-5">
<a href="/addOrEdit" class="btn btn-success"> <i
class="fas fa-user-plus ml-2"> Add User </i>
</a>
</p>


<select class="form-control pagination" id="pageSizeSelect">
<option th:each="pageSize : ${pageSizes}" th:text="${pageSize}"
th:value="${pageSize}"
th:selected="${pageSize} == ${selectedPageSize}"></option>
</select> <br></br>
<div th:if="${users.totalPages != 1}">
<ul class="pagination">
<li th:class="${users.number == 0} ? disabled"><a
class="page-link"
th:href="@{/(pageSize=${selectedPageSize}, page=1)}">&laquo;</a></li>
<li th:class="${users.number == 0} ? disabled"><a
class="page-link"
th:href="@{/(pageSize=${selectedPageSize}, page=${users.number})}">&larr;</a>
</li>
<li
th:class="${users.number == (page - 1)} ? 'active pointer-disabled'"
th:each="page : ${#numbers.sequence(pager.startPage, pager.endPage)}">
<a class="page-link"
th:href="@{/(pageSize=${selectedPageSize}, page=${page})}"
th:text="${page}"></a>
</li>
<li th:class="${users.number + 1 == users.totalPages} ? disabled">
<a class="page-link"
th:href="@{/(pageSize=${selectedPageSize}, page=${users.number + 2})}">&rarr;</a>
</li>
<li th:class="${users.number + 1 == users.totalPages} ? disabled">
<a class="page-link"
th:href="@{/(pageSize=${selectedPageSize}, page=${users.totalPages})}">&raquo;</a>
</li>
</ul>
</div>
<div th:case="*">
<div class="table-responsive-xl">
<table class="table">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.firstName}"></td>
<td th:text="${user.lastName}"></td>
<td th:text="${user.email}"></td>
<td><a th:href="@{/addOrEdit/{id}(id=${user.id})}"
class="btn btn-warning"> <i class="fas fa-user-edit ml-2"></i>
</a></td>
<td><a th:href="@{/delete/{id}(id=${user.id})}"
class="btn btn-danger"> <i class="fas fa-user-times ml-2"></i>
</a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script th:src="@{/webjars/jquery/3.4.0/jquery.min.js} "></script>
<script th:src="@{/webjars/bootstrap/4.3.1/js/bootstrap.min.js} "></script>

<script>
$(document).ready(() => {
changePageAndSize();
});

changePageAndSize = () => {
$('#pageSizeSelect').change(evt => {
window.location.replace(`/?pageSize=${evt.target.value}&page=1`);
});
}
</script>

</body>
</html>



Spring Boot Main Driver

package com.knf.dev.demo;

import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.knf.dev.demo.domain.User;
import com.knf.dev.demo.repository.UserRepository;

@SpringBootApplication
public class Application implements CommandLineRunner {

@Autowired
UserRepository repository;

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

@Override
public void run(String... args) throws Exception {

// Insert dummy users
User user1 = new User("Alpha", "Pro", "alpha@knf.net");
User user2 = new User("Beta", "Pro", "beta@knf.net");
User user3 = new User("Gama", "Gama Pro", "gama@knf.net");
User user4 = new User("Pekka", "pekka", "pekka@knf.net");
User user5 = new User("Tesla", "Pro", "tesla@knf.net");
User user6 = new User("Xray", "Noob", "xray@knf.net");
User user7 = new User("Tera", "noob", "tera@knf.net");
User user8 = new User("Bot", "noob", "alpha@knf.net");
User user9 = new User("Pro", "pro", "pro@knf.net");
List<User> users = Arrays.asList(user1, user2, user3,
user4, user9, user8, user7, user6, user5);
repository.saveAll(users);
}
}


Download the complete source code - click here                                        

Local Setup and Run the application

Step1: Download or clone the source code from GitHub to the local machine - Click here

Step 2: mvn clean install

Step 3: Run the Spring Boot application - mvn spring-boot:run

From the browser call the endpoint http://localhost:8080

More related topics,

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete