Spring Boot CRUD using Thymeleaf MySql

This CRUD example uses a product table, performs crud operations using spring boot and thymeleaf.

Directory Structure

Model class (product.java)

package com.example.demo.product;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Entity
public class Product {

	@Id 
	@GeneratedValue(strategy = GenerationType.AUTO) // auto increment ID
	private int id;

	@NotNull // variable should not be null
	@Size(min = 2, max = 30) // variables minimum size is 2 and max is 30
	private String name;

	@NotNull
	@Size(min = 12, max = 50)
	private String description;
	
	private boolean enabled;

	public Product() {
		super();
	}

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

	public String getName() {
		return name;
	}

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

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public boolean isEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	@Override
	public String toString() {
		return "ParentCategory [id=" + id + ", name=" + name + ", description=" + description + ", enabled=" + enabled
				+ "]";
	}

}

Table repository (ProductRepo.java)

package com.example.demo.product;

import org.springframework.data.repository.CrudRepository;

public interface ProductRepo extends CrudRepository<Product, Integer> {

}

Controller (ProductController.java)

package com.example.demo.product;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class ProductController {

	@Autowired
	ProductRepo productRepo;

	// Displays dashboard
	@GetMapping("/")
	public String index() {
		return "dashboard";
	}

	// Displays all records
	@GetMapping("/all")
	public String allProducts(Model model) {

		Iterable<Product> allProducts = productRepo.findAll();
		model.addAttribute("allProducts", allProducts);

		return "index";
	}

	// Displays single record
	@GetMapping("/{id}")
	public String showProduct(@PathVariable Integer id, Model model) {

		Product product = productRepo.findOne(id);

		if (null == product) {
			return null;
		} else {
			model.addAttribute("product", product);
			return "view";
		}
	}

	// render empty model for adding new record
	@GetMapping("/add")
	public String newProduct(Model model) {
		Product product = new Product();
		model.addAttribute("product", product);
		return "form";
	}

	// handles post request for adding record and updated record
	@PostMapping("/add")
	public String createNewProduct(@Valid Product product, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
			return "form";
		} else {
			System.out.println("-------------" + product.toString());

			productRepo.save(product);
			return "redirect:/" + product.getId();
		}
	}

	// render form for updating record
	@GetMapping("/edit/{id}")
	public String editProduct(@PathVariable(value = "id") Integer id, Model model) {

		Product product = productRepo.findOne(id);

		if (null == product) {
			return null;
		} else {
			model.addAttribute("product", product);
			return "form";
		}
	}

	// deletes a record
	@GetMapping("/delete/{id}")
	public String deleteProduct(@PathVariable(value = "id") Integer id) {

		Product product = productRepo.findOne(id);

		if (null == product) {
			return null;

		} else {
			productRepo.delete(id);
			return "redirect:/";
		}
	}

}

Header -template (header.html)

<div th:fragment="header-css" th:remove="tag">
	<!-- Bootstrap core CSS -->
	<link rel="stylesheet" th:href="@{/css/bootstrap.css}" />

	<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
	<link rel="stylesheet" th:href="@{/css/ie10-viewport-bug-workaround.css}" />

	<!-- Custom styles for this template -->
	<link rel="stylesheet" th:href="@{/css/sticky-footer-navbar.css}" />
	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />

	<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
	<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
	<script th:src="@{/admin/js/ie-emulation-modes-warning.js}"></script>

	<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
	<!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
</div>

<div th:fragment="header-navbar" th:remove="tag">

	<!-- Fixed navbar -->
	<nav class="navbar navbar-default navbar-fixed-top">
		<div class="container">
			<div class="navbar-header">
				<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
					<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span>
				</button>
				<a class="navbar-brand" href="#">CRUD</a>
			</div>
			<div id="navbar" class="collapse navbar-collapse">
				<ul class="nav navbar-nav">
					<li><a href="/">Home</a></li>
					<li><a href="/all">All Products</a></li>
				</ul>
			</div>
			<!--/.nav-collapse -->
		</div>
	</nav>

</div>

Footer -template (footer.html)

<div th:fragment="footer" th:remove="tag">
	<footer class="footer">
		<div class="container">
			<p class="text-muted">footer</p>
		</div>
	</footer>

	<!-- Bootstrap core JavaScript
    ================================================== -->
	<!-- Placed at the end of the document so the pages load faster -->
	<script th:src="@{/js/jquery.js}"></script>
	<script th:src="@{/js/bootstrap.js}"></script>

	<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
	<script th:src="@{/js/ie10-viewport-bug-workaround.js}"></script>
</div>

First index page (dashbaord.html)

<!DOCTYPE html>
<html lang="en">
<head>
<title>Dashbaord</title>
<div lang="en" th:replace="fragments/header :: header-css" th:remove="tag"></div>
</head>

<body>

	<div lang="en" th:replace="fragments/header :: header-navbar" th:remove="tag"></div>

	<!-- Begin page content -->
	<div class="container">
		<div class="page-header">
			<h1>CRUD App</h1>
		</div>		
	</div>

	<div lang="en" th:replace="fragments/footer :: footer" th:remove="tag"></div>

</body>
</html>

Display all products (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
<title>All Products</title>
<div lang="en" th:replace="fragments/header :: header-css" th:remove="tag"></div>
</head>

<body>

	<div lang="en" th:replace="fragments/header :: header-navbar" th:remove="tag"></div>

	<!-- Begin page content -->
	<div class="container">

		<div class="row">
			<div class="col-md-12">
			
			<h3>Parent Categories</h3>
			<hr />
			
			<div class="alert alert-danger" th:if="${deleteMessage}" th:utext="${deleteMessage + ' Deleted'}"></div>
			
				<table class="table table-bordered">
					<thead>
						<tr>
							<th>#</th>
							<th>Name</th>
							<th>Description</th>
							<th>Action</th>
						</tr>
					</thead>
					<tbody>
						<tr th:each="message : ${allProducts}">
							<td th:text="${message.id}">1</td>
							<td th:text="${message.name}">name</td>
							<td th:text="${message.description}">Description</td>
							<td><a th:href="@{${message.id}}" title="View"> <i class="fa fa-eye fa-lg"></i></a>
								<a th:href="@{'/edit/' + ${message.id}}" title="Update"> <i class="fa fa-pencil fa-lg"></i></a>
								<a th:href="@{'/delete/' + ${message.id}}" title="Update"> <i class="fa fa-trash fa-lg"></i></a>
							</td>
						</tr>
					</tbody>					
				</table>
				
				<a th:href="@{'/add'}" class="col-md-2">
					<button type="button" class="btn btn-primary">Add Category</button>
				</a>

			</div>
		</div>

	</div>

	<div lang="en" th:replace="fragments/footer :: footer" th:remove="tag"></div>

</body>
</html>

HTML form (form.html)

<!DOCTYPE html>
<html lang="en">
<head>
<title>Inside View</title>
<div lang="en" th:replace="fragments/header :: header-css" th:remove="tag"></div>
</head>

<body>

	<div lang="en" th:replace="fragments/header :: header-navbar" th:remove="tag"></div>

	<!-- Begin page content -->
	<div class="container">

		<div class="row">
			<div class="col-md-4">

				<form autocomplete="off" th:action="@{/add}" th:object="${product}" method="post" class="form-horizontal" role="form">

					<h2 class="text-center">Product</h2>

					<input type="hidden" class="form-control" th:field="*{id}" />

					<div class="form-group">
						<label for="name"> Name </label>
						<input type="text" th:field="*{name}" placeholder="Name" class="form-control" />
						<label th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert alert-danger"></label>
					</div>

					<div class="form-group">
						<label for="description"> Description </label>
						<input type="text" th:field="*{description}" placeholder="Description" class="form-control" />
						<label th:if="${#fields.hasErrors('description')}" th:errors="*{description}" class="alert alert-danger"></label>
					</div>
					
					<div class="form-group">
						<label for="description"> Enabled </label>
						<select class="form-control" th:field="*{enabled}">
							<option th:value="${true}">Enabled</option>
							<option th:value="${false}">Disabled</option>
						</select>
					</div>

					<div class="form-group">
						<input type="submit" class="btn btn-primary btn-block" value="Save" />
					</div>

				</form>

			</div>
		</div>

	</div>

	<div lang="en" th:replace="fragments/footer :: footer" th:remove="tag"></div>

</body>
</html>

viewing single product (view.html)

<!DOCTYPE html>
<html lang="en">
<head>
<title>Inside View</title>
<div lang="en" th:replace="fragments/header :: header-css" th:remove="tag"></div>
</head>

<body>

	<div lang="en" th:replace="fragments/header :: header-navbar" th:remove="tag"></div>

	<!-- Begin page content -->
	<div class="container">

		<div class="row">
			<div class="col-md-12">

				<h4 th:text="${product.name + '- view'}"></h4>
				<hr />

				<table class="table table-bordered">
					<thead>
						<tr>
							<th style="width: 20%">Id</th>
							<td th:text="${product.id}">Id</td>
						</tr>
						<tr>
							<th>Name</th>
							<td th:text="${product.name}">Name</td>
						</tr>
						<tr>
							<th>Description</th>
							<td th:text="${product.description}">Description</td>
						</tr>
						<tr>
							<th>Enabled</th>
							<td th:text="${product.enabled}">Enabled</td>
						</tr>

					</thead>

				</table>

			</div>
		</div>

	</div>

	<div lang="en" th:replace="fragments/footer :: footer" th:remove="tag"></div>

</body>
</html>

Properties (application.properties)

spring.thymeleaf.cache=false

spring.datasource.url=jdbc:mysql://localhost:3306/product?useSSL=false
spring.datasource.username= root
spring.datasource.password= 1234
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.ddl-auto = update

Outputs

Project File

spring-thymeleaf-crud.zip

Leave a Reply

Your email address will not be published. Required fields are marked *