Spring Boot + Thymeleaf CRUD Operation

By Arvind Rai, October 11, 2023
Spring Boot and Thyemeleaf together make it easy to create CRUD operation. CRUD is Create, Read, Update and Delete operation. Spring Data CrudRepository minimises the coding to perform persistence query. Thyemeleaf can be integrated with Spring for web application. Spring MVC works with Thyemeleaf same as JSP. We need to use Spring EL in our Thymeleaf template instead of OGNL. HTML form is created in template that will be completely integrated with form-backing beans and result bindings. We can easily perform validation error handling within template. We can display internationalization messages from messages files as usual.
Here on this page I will create a Spring Boot CRUD application using Thymeleaf and MySQL. Find the project structure screenshot.
Spring Boot Thymeleaf CRUD Example

1. Maven Dependencies

Find the Maven dependencies for Spring Boot, Thyemleaf and MySQL.
pom.xml
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.1.3</version>
	<relativePath />
</parent>
<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>
		<version>3.1.3</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-validation</artifactId>
	</dependency>
	<dependency>
		<groupId>jakarta.persistence</groupId>
		<artifactId>jakarta.persistence-api</artifactId>
		<version>3.1.0</version>
	</dependency>
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>8.0.32</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<optional>true</optional>
	</dependency>
</dependencies> 

2. Connecting to Database

1. For CRUD operation I am using MySQL. Find the table created in our application.
CREATE TABLE `employee` (
  `emp_id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `gender` char(10) NOT NULL,
  `married` char(1) NOT NULL,
  `profile` varchar(50) NOT NULL,
  PRIMARY KEY (`emp_id`)
) 
When we create new employee, the employee id will be generated automatically.
2. Spring Boot uses HikariCP connection by default. Find the datasource connection configuration.
application.properties
spring.datasource.url=jdbc:mysql://localhost/cp
spring.datasource.username=root
spring.datasource.password=Mysql@1234
spring.datasource.type = com.zaxxer.hikari.HikariDataSource 

3. Using CrudRepository

The CrudRepository is a Spring Data interface that is used for generic CRUD operations on a repository for a specific type.
For CRUD operation I am using its following methods.
save(S entity) : Saves the specified entity and returns updated one. If id of entity is already existing in the persistence, then update operation is performed against that id.
deleteById(ID id) : Deletes the entity by the given id. If id is not available in the persistence, then it is silently ignored.
findAll() : Returns all the elements.
findById(ID id) : Returns the element by id.

Find the CrudRepository used in our application.
EmployeeRepository.java
@Repository
public interface EmployeeRepository extends CrudRepository  {
	@Modifying
	@Transactional
	@Query(value= "INSERT INTO Employee (empName, gender, isMarried, profile) VALUES (:empName, :gender, :isMarried, :profile)")
           void saveEmp(@Param("empName") String empName, @Param("gender") String gender, @Param("isMarried") Boolean isMarried, @Param("profile") String profile);
} 
At runtime Spring creates EmployeeRepository instance and defines all its methods. We can autowire EmployeeRepository to get its instance to perform transaction.
We can add custom methods using @Query annotation.
@Query : Annotation to declare a query.
@Modifying : The query that involves INSERT, UPDATE, DELETE is marked as modifying query.
@Transactional : Marks the method as transactional. If DDL transaction fails, it will rollback.

The save(S entity) method of CrudRepository can perform insert and update operation both. If entity id is already there then update operation otherwise insert operation is performed.
I have created a method saveEmp() that will insert columns except entity id. In my application, id is generated automatically.

Find the entity.
Employee.java
@Entity
@Table(name = "employee")
public class Employee implements Serializable {
	private static final long serialVersionUID = 1L;
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "emp_id")
	private Integer empId;
	@NotNull
	@Size(min = 5, max = 15)
	@Column(name = "name")
	private String empName;
	@NotNull
	@Column(name = "gender")
	private String gender;
	@Column(name = "married")
	private Boolean isMarried;
	@Column(name = "profile")
	private String profile;
	//Setters and Getters
} 


4. Service Layer

I have created service class using EmployeeRepository to perform CRUD operation.
EmployeeService.java
@Service
public class EmployeeService {
	@Autowired
	private EmployeeRepository empRepository;

	public Iterable<Employee> getAllEmployees() {
		Iterable<Employee> emps = empRepository.findAll();
                      return emps;
	}
	public Employee getEmployeeByI(int id) {
		return empRepository.findById(id).get();
	}
	public void addEmployee(Employee emp) {
		empRepository.saveEmp(emp.getEmpName(), emp.getGender(), emp.getIsMarried(), emp.getProfile());
	}
	public void updateEmployee(Employee emp) {
		empRepository.save(emp);
	}	
	public void deleteEmployee(Integer empId) {
		empRepository.deleteById(empId);
	}	
} 

5. Controller Layer

Find the controller that handles CRUD operation.
EmployeeController.java
@Controller
@RequestMapping("app")
public class EmployeeController {
	private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
	@Autowired
	private EmployeeService empService;

	@GetMapping("create-emp")
	public ModelAndView createEmpView() {
		ModelAndView mav = new ModelAndView();
		mav.setViewName("new-employee");
		mav.addObject("emp", new Employee());
		mav.addObject("allProfiles", getProfiles());
		return mav;
	}

	@PostMapping("create-emp")
	public ModelAndView createEmployee(@Valid @ModelAttribute("emp") Employee emp, BindingResult result) {
		ModelAndView mav = new ModelAndView();
		if (result.hasErrors()) {
			logger.info("Validation errors while submitting form.");
			mav.setViewName("new-employee");
			mav.addObject("emp", emp);
			mav.addObject("allProfiles", getProfiles());
			return mav;
		}
		if (emp.getEmpId() == null) {
			empService.addEmployee(emp);
		} else {
			empService.updateEmployee(emp);
		}
		mav.addObject("allEmps", empService.getAllEmployees());
		mav.setViewName("emp-info");
		logger.info("Form submitted successfully.");
		return mav;
	}

	@GetMapping("all-emp")
	public ModelAndView getAllEmployees() {
		ModelAndView mav = new ModelAndView();
		mav.addObject("allEmps", empService.getAllEmployees());
		mav.setViewName("emp-info");
		return mav;
	}

	@GetMapping("get-emp")
	public ModelAndView getEmp(@RequestParam("id") Integer empId) {
		ModelAndView mav = new ModelAndView();
		mav.setViewName("new-employee");
		mav.addObject("emp", empService.getEmployeeByI(empId));
		mav.addObject("allProfiles", getProfiles());
		return mav;
	}

	@DeleteMapping("delete-emp/{id}")
	public ModelAndView deleteEmp(@PathVariable("id") Integer empId) {
		empService.deleteEmployee(empId);
		ModelAndView mav = new ModelAndView();
		mav.addObject("allEmps", empService.getAllEmployees());
		mav.setViewName("emp-info");
		return mav;
	}

	private List<String> getProfiles() {
		List<String> list = new ArrayList<>();
		list.add("Developer");
		list.add("Manager");
		list.add("Director");
		return list;
	}
} 

@Controller : Stereotype annotation to mark a class as controller.
@RequestMapping : Annotation for mapping web requests.
@GetMapping : Maps HTTP GET requests.
@PostMapping : Maps HTTP POST requests.
@DeleteMapping : Maps HTTP DELETE requests.
@Valid : Jakarta validation that marks a property to be validated.
@ModelAttribute : Accesses the attribute from a model.
BindingResult : Represents binding result. We can use it to check if the property has validation error.
ModelAndView : Handles Model and View.
@PathVariable : Annotation to bind URI template variable.

6. View Layer

To create view for our CRUD operation, I am using Thymeleaf template engine. Thymeleaf is server-side Java template engine for both web and standalone environment.
Find the view that will be used to create and update the employee.
new-employee.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:text="#{app.title}"> Title </title>
        <link rel="stylesheet" th:href="@{/css/styles.css}"/>
    </head>
	<body>
		<h1 th:text="#{app.create-emp}" th:unless="${emp.getEmpId()}">Create</h1>
		<h1 th:text="#{app.update-emp}" th:if="${emp.getEmpId()}">Update</h1>
	     <form action="#" th:action="@{/app/create-emp}" th:object="${emp}" method="POST">
	     <table>
	    	<tr th:if="${emp.getEmpId()}"><td th:text="#{emp.id}"></td>
    	        <td>
    	          <input type="text" th:field="*{empId}" readonly/>
    	        </td>
    	     </tr>
	    	<tr><td th:text="#{emp.name}"></td>
	    	    <td>
	    	      <input type="text" th:field="*{empName}" />
	    	      <br/><label th:if="${#fields.hasErrors('empName')}" th:text="#{error.emp.name}" th:class="'error'">Name Error</label>
	    	    </td>
	    	</tr>
	    	<tr><td th:text="#{emp.gender}"></td>
	    	   <td>
	    	       <input type="radio" th:field="*{gender}" value="Male"/><label th:text="#{emp.gender.male}">M</label>
	    	       <input type="radio" th:field="*{gender}" value="Female"/><label th:text="#{emp.gender.female}">F</label>
	    	       <br/><label th:if="${#fields.hasErrors('gender')}" th:text="#{error.emp.gender}" th:class="'error'">Gender Error</label>
	    	   </td>
	    	</tr>
	    	<tr><td th:text="#{emp.married}+ '?'"></td>
	    	   <td>
	    	       <input type="checkbox" th:field="*{isMarried}" />
	    	   </td>
	    	</tr>	    	
	    	<tr><td th:text="#{emp.profile}"></td>
	    	   <td>
	    	       <select th:field="*{profile}">
	    	          <option th:each="profile : ${allProfiles}" th:value="${profile}" th:text="${profile}">Profile</option>
	    	       </select>
	    	   </td>
	    	</tr>	    		    	
	        <tr><td colspan="2">
	              <input type="submit" th:value="#{emp.form.create}" th:unless="${emp.getEmpId()}"/> 
	              <input type="submit" th:value="#{emp.form.update}" th:if="${emp.getEmpId()}"/> 
	              <input type="reset" th:value="#{emp.form.reset}" th:unless="${emp.getEmpId()}"/>
	            </td>
	        </tr>
	      </table>  
	    </form>
	    <br/><a href="#" th:href="@{/app/all-emp}" th:text="#{emp.emp-list}">List</a>
	</body>
</html> 

th:text : Externalises the text so that they can be kept in .properties file.
th:href : Modifier attribute that computes the link URL once processed.
th:field : Binds the input with a property.
th:each : Iterates Java Iterable, Map, List and Array.
th:value : Value of HTML element.
th:action : Action of Form.
th:object : Local variable definition.
th:if : Simple conditional if.
th:unless : Opposite of th:if. The condition which is false for th:if, is true for th:unless.

Find the view that displays all employees. Here I have created link to load employee for update and link to delete the employee.
emp-info.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
      <title th:text="#{app.title}"> Title </title>
      <link rel="stylesheet" th:href="@{/css/styles.css}"/>
	  <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
	  <script>
	    const httpDelete = (urlToDelete) => {
	      $.ajax({ 
	          url: urlToDelete, 
	          type: 'DELETE', 
	          success: function (result) { 
	        	  window.location = "/app/all-emp";
	          } 
	      }); 
	    };
	  </script>
</head>
<body>
   <h1 th:text="#{app.emp-details}">Details</h1>
    <table>
        <tr>
           <th th:text="#{emp.id}"/></th>
           <th th:text="#{emp.name}"></th>
           <th th:text="#{emp.gender}"></th>
           <th th:text="#{emp.married}+ '?'"></th>
           <th th:text="#{emp.profile}"></th>
           <th></th><th></th>
        </tr>
        <tr th:each="emp : ${allEmps}">
	             <td th:text="${emp.empId}">Id</td>
		  <td th:text="${emp.empName}">Title</td>
		  <td th:text="${emp.gender}">Gender</td>
		  <td th:text="${emp.isMarried}">Married</td>
		  <td th:text="${emp.profile}">Profile</td>	
		  <td><a href="#" th:href="@{'/app/get-emp?id='+${emp.empId}}" th:text="#{emp.update}">Update</a></td>
		  <td><a href="#" th:href="'javascript:httpDelete(\''+@{'/app/delete-emp/'+${emp.empId}}+'\')'" th:text="#{emp.delete}">Delete</a></td>
        </tr>	
   </table>
	<br/><a href="#" th:href="@{/app/create-emp}" th:text="#{emp.add-more-emp}">Add</a>
</body>
</html> 
To send a URL with HTTP DELETE method, I am using jQuery in my application. We can use also XMLHttpRequest as given below.
 const request = new XMLHttpRequest();
 request.open('DELETE', url, true);
 request.send(); 
Now find the property file used to internationalize the messages.
messages.properties
app.title= Spring Boot + Thymeleaf
app.create-emp= Create Employee
app.update-emp= Update Employee
app.emp-details= Employee Details

emp.id= Id
emp.name= Name
emp.gender= Gender
emp.married= Married
emp.profile= Profile

emp.gender.male= Male
emp.gender.female= Female

emp.form.create= Create
emp.form.update= Update 
emp.form.reset= Reset  
emp.add-more-emp= Add More Employees
emp.update= Update
emp.delete= Delete
emp.emp-list= Employee List

error.emp.name= User name must be between 5 to 15 characters.
error.emp.gender= Select  gender. 

7. Run Application

Find the main class to run the Spring Boot application.
MyApplication.java
@SpringBootApplication
@EnableJpaRepositories("com.concretepage.repository")
public class MyApplication {  
	public static void main(String[] args) {
		SpringApplication.run(MyApplication.class, args);
    }       
} 
Output screen to add the employee.
Spring Boot Thymeleaf CRUD Example
Output screen that displays the employee list.
Spring Boot Thymeleaf CRUD Example

8. References

9. Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
LEARN MORE












©2024 concretepage.com | Privacy Policy | Contact Us