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