In this tutorial, we’re gonna build a Spring Boot Thymeleaf CRUD example with Maven that use Spring Data JPA to interact with H2/MySQL/PostgreSQL database. You’ll know:
- How to configure Spring Data, JPA, Hibernate to work with Database
- How to define Data Entity and Repository interfaces
- Way to create Spring Controller to process HTTP requests
- Way to use Spring Data JPA to interact with H2/MySQL/PostgreSQL Database
- How to use Thymeleaf template engine for View layer
Fullstack:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– React + Spring Boot example
Exception Handling:
– Spring Boot @ControllerAdvice & @ExceptionHandler example
– @RestControllerAdvice example in Spring Boot
Testing:
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller
Associations:
– Spring Boot One To One example with JPA, Hibernate
– Spring Boot One To Many example with JPA, Hibernate
– Spring Boot Many to Many example with JPA, Hibernate
Contents
Overview of Spring Boot Thymeleaf example
We will build a Spring Boot CRUD example using Thymeleaf template engine for View layer and Spring Data JPA with Database in that:
- Each Tutorial (entity) has id, title, description, level, published status.
- CRUD operations are supported: create, retrieve, update, delete Tutorials.
- User can search Tutorials by title.
– Create new entity object:
– Retrieve all objects:
– Update status of an item:
– Update/Edit an object in its own page:
– Search Tutorials by title:
– Delete an item:
If you want to add Pagination, please visit:
Spring Boot Thymeleaf Pagination example
Technology
- Java 8
- Spring Boot 2.7 (with Spring Web MVC, Spring Data JPA, Thymeleaf)
- H2/MySQL/PostgreSQL Database
- Maven 3.6.1
- Bootstrap 4
- jQuery 3.6.1
- Font Awesome
Project Structure
Let me explain it briefly.
– Tutorial
class corresponds to entity and table tutorials.
– TutorialRepository
is an interface that extends JpaRepository for CRUD methods and custom finder methods. It will be autowired in TutorialController
.
– TutorialController
is a Controller which has request mapping methods for RESTful requests such as: getAll, addTutorial, saveTutorial, editTutorial, deleteTutorial, updateTutorialPublishedStatus.
– static/css
contains custom css style.
– template
stores HTML template files for the project.
– Configuration for Spring Datasource, JPA & Hibernate in application.properties.
– pom.xml contains dependencies for Spring Boot, Thymeleaf, Bootstrap and Database.
We can improve the example by adding Comments for each Tutorial. It is the One-to-Many Relationship and I write a tutorial for this at:
Spring Boot One To Many example with JPA, Hibernate
Or add Tags with Many-to-Many Relationship:
Spring Boot Many to Many example with JPA, Hibernate
Create & Setup Spring Boot project
Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.
Then open pom.xml and add these 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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.6.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
We also need to add one more dependency.
– If you want to use MySQL:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
– or PostgreSQL:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
– or H2 (embedded database):
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Configure Spring Boot, JPA, Datasource, Hibernate
Under src/main/resources folder, open application.properties and write these lines.
– For MySQL:
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false
spring.datasource.username= root
spring.datasource.password= 123456
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQL5InnoDBDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update
– For PostgreSQL:
spring.datasource.url= jdbc:postgresql://localhost:5432/testdb
spring.datasource.username= postgres
spring.datasource.password= 123
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto= update
spring.datasource.username
&spring.datasource.password
properties are the same as your database installation.- Spring Boot uses Hibernate for JPA implementation, we configure
MySQL5InnoDBDialect
for MySQL orPostgreSQLDialect
for PostgreSQL spring.jpa.hibernate.ddl-auto
is used for database initialization. We set the value toupdate
value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should bevalidate
.
– For H2 database:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update
spring.h2.console.enabled=true
# default path: h2-console
spring.h2.console.path=/h2-ui
spring.datasource.url
:jdbc:h2:mem:[database-name]
for In-memory database andjdbc:h2:file:[path/database-name]
for disk-based database.- We configure
H2Dialect
for H2 Database spring.h2.console.enabled=true
tells the Spring to start H2 Database administration tool and you can access this tool on the browser:http://localhost:8080/h2-console
.spring.h2.console.path=/h2-ui
is for H2 console’s url, so the default urlhttp://localhost:8080/h2-console
will change tohttp://localhost:8080/h2-ui
.
Define Data Entity
Our main Entity is Tutorial
with several fields: id, title, description, level, published.
In entity package, we define Tutorial
class.
entity/Tutorial.java
package com.bezkoder.spring.thymeleaf.entity;
import javax.persistence.*;
@Entity
@Table(name = "tutorials")
public class Tutorial {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(length = 128, nullable = false)
private String title;
@Column(length = 256)
private String description;
@Column(nullable = false)
private int level;
@Column
private boolean published;
public Tutorial() {
}
public Tutorial(String title, String description, int level, boolean published) {
this.title = title;
this.description = description;
this.level = level;
this.published = published;
}
// getters and setters
}
– @Entity
annotation indicates that the class is a persistent Java class.
– @Table
annotation provides the table that maps this entity.
– @Id
annotation is for the primary key.
– @GeneratedValue
annotation is used to define generation strategy for the primary key. GenerationType.AUTO
means Auto Increment field.
– @Column
annotation is used to define the column in database that maps annotated field.
Create Repository Interface
Let’s create a repository to interact with Tutorial entity from the database.
In repository package, create TutorialRepository
interface that extends JpaRepository
.
repository/TutorialRepository.java
package com.bezkoder.spring.thymeleaf.repository;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.thymeleaf.entity.Tutorial;
@Repository
@Transactional
public interface TutorialRepository extends JpaRepository<Tutorial, Integer> {
List<Tutorial> findByTitleContainingIgnoreCase(String keyword);
@Query("UPDATE Tutorial t SET t.published = :published WHERE t.id = :id")
@Modifying
public void updatePublishedStatus(Integer id, boolean published);
}
Now we can use JpaRepository’s methods: save()
, findOne()
, findById()
, findAll()
, count()
, delete()
, deleteById()
… without implementing these methods.
We also define custom methods:
– findByTitleContainingIgnoreCase()
: returns all Tutorials which title
contains the input keyword
.
– updatePublishedStatus()
: update published
field of the Tutorial entity specified by id
.
The implementation is plugged in by Spring Data JPA automatically.
More Derived queries at:
JPA Repository query example in Spring Boot
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
You can modify this Repository:
– to work with Pagination, the instruction can be found at:
Spring Boot Pagination & Filter example | Spring JPA, Pageable
– or to sort/order by multiple fields with the tutorial:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot
You also find way to write Unit Test for this JPA Repository at:
Spring Boot Unit Test for JPA Repository with @DataJpaTest
Create the MVC Controller
let’s create a controller that handles requests. It injects TutorialRepository
for creating, retrieving, updating, deleting and finding Tutorials.
controller/TutorialController.java
package com.bezkoder.spring.thymeleaf.controller;
// ...
import com.bezkoder.spring.thymeleaf.entity.Tutorial;
import com.bezkoder.spring.thymeleaf.repository.TutorialRepository;
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public String getAll(Model model, @Param("keyword") String keyword) {
...
return "tutorials";
}
@GetMapping("/tutorials/new")
public String addTutorial(Model model) {
...
return "tutorial_form";
}
@PostMapping("/tutorials/save")
public String saveTutorial(Tutorial tutorial, RedirectAttributes redirectAttributes) {
...
return "redirect:/tutorials";
}
@GetMapping("/tutorials/{id}")
public String editTutorial(@PathVariable("id") Integer id, Model model, RedirectAttributes redirectAttributes) {
...
return "tutorial_form";
}
@GetMapping("/tutorials/delete/{id}")
public String deleteTutorial(@PathVariable("id") Integer id, Model model, RedirectAttributes redirectAttributes) {
...
return "redirect:/tutorials";
}
@GetMapping("/tutorials/{id}/published/{status}")
public String updateTutorialPublishedStatus(@PathVariable("id") Integer id, @PathVariable("status") boolean published,
Model model, RedirectAttributes redirectAttributes) {
...
return "redirect:/tutorials";
}
}
Setup the Template
In src/main/resources folder, create folder and file as following structure:
Header and Footer
We will use Thymeleaf Fragments (th:fragment
) to reuse some common parts such as header and footer.
Let’s write HTML code for them.
fragments/footer.html
<footer class="text-center">
Copyright © BezKoder
</footer>
And header contains the navigation bar:
fragments/header.html
<header th:fragment="header">
<nav class="navbar navbar-expand-md bg-dark navbar-dark">
<a class="navbar-brand" th:href="@{/tutorials}">
BezKoder
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="topNavbar">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" th:href="@{/tutorials}">Tutorials</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/tutorials/new}">Add</a>
</li>
</ul>
</div>
</nav>
</header>
Form and List
Now we need to create HTML files, then import Thymeleaf fragments, Bootstrap, jQuery and Font Awesome.
tutorial-form.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0" />
<title>BezKoder - Spring Boot Thymeleaf example</title>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />
<script type="text/javascript" th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
<div th:replace="fragments/header :: header"></div>
-- form --
<div th:replace="fragments/footer :: footer"></div>
</body>
</html>
tutorials.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0" />
<title>BezKoder - Spring Boot Thymeleaf example</title>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<script type="text/javascript" th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
<div th:replace="fragments/header :: header"></div>
-- list --
<div th:replace="fragments/footer :: footer"></div>
</body>
</html>
Implement CRUD Operations
Retrieve Entities
TutorialController.java
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public String getAll(Model model) {
try {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
tutorialRepository.findAll().forEach(tutorials::add);
model.addAttribute("tutorials", tutorials);
} catch (Exception e) {
model.addAttribute("message", e.getMessage());
}
return "tutorials";
}
}
tutorials.html
<div th:if="${message != null}" class="alert alert-success alert-dismissible fade show text-center message"
role="alert">
[[${message}]]
<button type="button" class="close btn-sm" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div th:if="${tutorials.size() > 0}">
<table class="table table-hover table-responsive-xl">
<thead class="thead-light">
<tr>
<th scope="col">Id</th>
<th scope="col">Title</th>
<th scope="col">Description</th>
<th scope="col">Level</th>
<th scope="col">Published</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="tutorial : ${tutorials}">
<th scope="row">[[${tutorial.id}]]</th>
<td>[[${tutorial.title}]]</td>
<td>[[${tutorial.description}]]</td>
<td>[[${tutorial.level}]]</td>
<td>
<a th:if="${tutorial.published == true}" class="fa-regular fa-square-check"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/false'}" title="Disable this tutorial"></a>
<a th:if="${tutorial.published == false}" class="fa-regular fa-square icon-dark"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/true'}" title="Enable this tutorial"></a>
</td>
<td>
<a th:href="@{'/tutorials/' + ${tutorial.id}}" title="Edit this tutorial"
class="fa-regular fa-pen-to-square icon-dark"></a>
<a th:href="@{'/tutorials/delete/' + ${tutorial.id}}" th:tutorialTitle="${tutorial.title}" id="btnDelete"
title="Delete this tutorial" class="fa-regular fa-trash-can icon-dark btn-delete"></a>
</td>
</tr>
</tbody>
</table>
</div>
Create Entity
controller/TutorialController.java
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials/new")
public String addTutorial(Model model) {
Tutorial tutorial = new Tutorial();
tutorial.setPublished(true);
model.addAttribute("tutorial", tutorial);
model.addAttribute("pageTitle", "Create new Tutorial");
return "tutorial_form";
}
@PostMapping("/tutorials/save")
public String saveTutorial(Tutorial tutorial, RedirectAttributes redirectAttributes) {
try {
tutorialRepository.save(tutorial);
redirectAttributes.addFlashAttribute("message", "The Tutorial has been saved successfully!");
} catch (Exception e) {
redirectAttributes.addAttribute("message", e.getMessage());
}
return "redirect:/tutorials";
}
}
tutorial-form.html
<h2 class="text-center">[[${pageTitle}]]</h2>
<div class="my-3">
<form th:action="@{/tutorials/save}" method="post" enctype="multipart/form-data" th:object="${tutorial}"
style="max-width: 550px; margin: 0 auto">
<input type="hidden" th:field="*{id}" />
<div class="p-3">
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="title">Title</label>
<div class="col-sm-9">
<input type="text" th:field="*{title}" required minlength="2" maxlength="128" class="form-control"
id="title" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="description">Description</label>
<div class="col-sm-9">
<input type="text" th:field="*{description}" minlength="2" maxlength="256" class="form-control"
id="description" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="level">Level</label>
<div class="col-sm-9">
<input type="number" step="1" th:field="*{level}" required min="1"
class="form-control" id="level" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 form-check-label" for="published">Published</label>
<div class="col-sm-9">
<input type="checkbox" th:field="*{published}" />
</div>
</div>
<div class="text-center">
<input type="submit" value="Save" class="btn btn-primary btn-sm mr-2" />
<input type="button" value="Cancel" id="btnCancel" class="btn btn-secondary btn-sm" />
</div>
</div>
</form>
</div>
Update Entity
tutorials.html
<td>
<a th:if="${tutorial.published == true}" class="fa-regular fa-square-check"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/false'}" title="Disable this tutorial"></a>
<a th:if="${tutorial.published == false}" class="fa-regular fa-square icon-dark"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/true'}" title="Enable this tutorial"></a>
</td>
<td>
<a th:href="@{'/tutorials/' + ${tutorial.id}}" title="Edit this tutorial"
class="fa-regular fa-pen-to-square icon-dark"></a>
...
</td>
TutorialController.java
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@PostMapping("/tutorials/save")
public String saveTutorial(Tutorial tutorial, RedirectAttributes redirectAttributes) {
try {
tutorialRepository.save(tutorial);
redirectAttributes.addFlashAttribute("message", "The Tutorial has been saved successfully!");
} catch (Exception e) {
redirectAttributes.addAttribute("message", e.getMessage());
}
return "redirect:/tutorials";
}
@GetMapping("/tutorials/{id}")
public String editTutorial(@PathVariable("id") Integer id, Model model, RedirectAttributes redirectAttributes) {
try {
Tutorial tutorial = tutorialRepository.findById(id).get();
model.addAttribute("tutorial", tutorial);
model.addAttribute("pageTitle", "Edit Tutorial (ID: " + id + ")");
return "tutorial_form";
} catch (Exception e) {
redirectAttributes.addFlashAttribute("message", e.getMessage());
return "redirect:/tutorials";
}
}
@GetMapping("/tutorials/{id}/published/{status}")
public String updateTutorialPublishedStatus(@PathVariable("id") Integer id, @PathVariable("status") boolean published,
Model model, RedirectAttributes redirectAttributes) {
try {
tutorialRepository.updatePublishedStatus(id, published);
String status = published ? "published" : "disabled";
String message = "The Tutorial id=" + id + " has been " + status;
redirectAttributes.addFlashAttribute("message", message);
} catch (Exception e) {
redirectAttributes.addFlashAttribute("message", e.getMessage());
}
return "redirect:/tutorials";
}
}
tutorial-form.html
<h2 class="text-center">[[${pageTitle}]]</h2>
<div class="my-3">
<form th:action="@{/tutorials/save}" method="post" enctype="multipart/form-data" th:object="${tutorial}"
style="max-width: 550px; margin: 0 auto">
<input type="hidden" th:field="*{id}" />
<div class="p-3">
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="title">Title</label>
<div class="col-sm-9">
<input type="text" th:field="*{title}" required minlength="2" maxlength="128" class="form-control"
id="title" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="description">Description</label>
<div class="col-sm-9">
<input type="text" th:field="*{description}" minlength="2" maxlength="256" class="form-control"
id="description" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label" for="level">Level</label>
<div class="col-sm-9">
<input type="number" step="1" th:field="*{level}" required min="1"
class="form-control" id="level" />
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 form-check-label" for="published">Published</label>
<div class="col-sm-9">
<input type="checkbox" th:field="*{published}" />
</div>
</div>
<div class="text-center">
<input type="submit" value="Save" class="btn btn-primary btn-sm mr-2" />
<input type="button" value="Cancel" id="btnCancel" class="btn btn-secondary btn-sm" />
</div>
</div>
</form>
</div>
<script type="text/javascript">
$(document).ready(function () {
$("#btnCancel").on("click", function () {
window.location = "[[@{/tutorials}]]";
});
});
</script>
Delete Entity
TutorialController.java
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials/delete/{id}")
public String deleteTutorial(@PathVariable("id") Integer id, Model model, RedirectAttributes redirectAttributes) {
try {
tutorialRepository.deleteById(id);
redirectAttributes.addFlashAttribute("message", "The Tutorial with id=" + id + " has been deleted successfully!");
} catch (Exception e) {
redirectAttributes.addFlashAttribute("message", e.getMessage());
}
return "redirect:/tutorials";
}
}
tutorials.html
<td>
...
<a th:href="@{'/tutorials/delete/' + ${tutorial.id}}" th:tutorialTitle="${tutorial.title}" id="btnDelete"
title="Delete this tutorial" class="fa-regular fa-trash-can icon-dark btn-delete"></a>
</td>
<div class="modal fade text-center" id="confirmModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Confirmation</h5>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<span id="confirmText"></span>
</div>
<div class="modal-footer">
<a type="button" id="yesBtn" class="btn btn-danger">Yes</a>
<button type="button" class="btn btn-secondary" data-dismiss="modal">No</button>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
$(".btn-delete").on("click", function (e) {
e.preventDefault();
link = $(this);
tutorialTitle = link.attr("tutorialTitle");
$("#yesBtn").attr("href", link.attr("href"));
$("#confirmText").html("Do you want to delete the Tutorial \<strong\>" + tutorialTitle + "\<\/strong\>?");
$("#confirmModal").modal();
});
});
</script>
Search Entities
TutorialController.java
@Controller
public class TutorialController {
@Autowired
private TutorialRepository tutorialRepository;
@GetMapping("/tutorials")
public String getAll(Model model, @Param("keyword") String keyword) {
try {
List<Tutorial> tutorials = new ArrayList<Tutorial>();
if (keyword == null) {
tutorialRepository.findAll().forEach(tutorials::add);
} else {
tutorialRepository.findByTitleContainingIgnoreCase(keyword).forEach(tutorials::add);
model.addAttribute("keyword", keyword);
}
model.addAttribute("tutorials", tutorials);
} catch (Exception e) {
model.addAttribute("message", e.getMessage());
}
return "tutorials";
}
}
tutorials.html
<div class="my-3">
<form th:action="@{/tutorials}">
<div class="row d-flex">
<div class="col-md-6 mt-2">
<div class="search">
<i class="fa fa-search"></i>
<input id="keyword" type="search" name="keyword" th:value="${keyword}" required class="form-control"
placeholder="Enter keyword">
<button type="submit" class="btn btn-secondary">Search</button>
</div>
</div>
<div class="col-md-6 mt-2">
<button id="btnClear" class="btn btn-info">Clear</button>
</div>
</div>
</form>
</div>
<div th:if="${tutorials.size() > 0}">
<table class="table table-hover table-responsive-xl">
<thead class="thead-light">
<tr>
<th scope="col">Id</th>
<th scope="col">Title</th>
<th scope="col">Description</th>
<th scope="col">Level</th>
<th scope="col">Published</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="tutorial : ${tutorials}">
<th scope="row">[[${tutorial.id}]]</th>
<td>[[${tutorial.title}]]</td>
<td>[[${tutorial.description}]]</td>
<td>[[${tutorial.level}]]</td>
<td>
<a th:if="${tutorial.published == true}" class="fa-regular fa-square-check"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/false'}" title="Disable this tutorial"></a>
<a th:if="${tutorial.published == false}" class="fa-regular fa-square icon-dark"
th:href="@{'/tutorials/' + ${tutorial.id} + '/published/true'}" title="Enable this tutorial"></a>
</td>
<td>
<a th:href="@{'/tutorials/' + ${tutorial.id}}" title="Edit this tutorial"
class="fa-regular fa-pen-to-square icon-dark"></a>
<a th:href="@{'/tutorials/delete/' + ${tutorial.id}}" th:tutorialTitle="${tutorial.title}" id="btnDelete"
title="Delete this tutorial" class="fa-regular fa-trash-can icon-dark btn-delete"></a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="" th:unless="${tutorials.size() > 0}">
<span>No tutorials found!</span>
</div>
<script type="text/javascript">
$(document).ready(function () {
// ...
$("#btnClear").on("click", function (e) {
e.preventDefault();
$("#keyword").text("");
window.location = "[[@{/tutorials}]]";
});
});
</script>
Run the Spring Boot Thymepleaf example
Run Spring Boot application with command: mvn spring-boot:run
.
Conclusion
Today we’ve built a Rest CRUD API using Spring Boot, Spring Data JPA working with H2 Database example.
We also see that JpaRepository
supports a great way to make CRUD operations and custom finder methods without need of boilerplate code.
More Derived queries at:
JPA Repository query example in Spring Boot
Custom query with @Query
annotation:
Spring JPA @Query example: Custom query in Spring Boot
To sort/order by multiple fields:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot
Handle Exception for this Rest APIs is necessary:
– Spring Boot @ControllerAdvice & @ExceptionHandler example
– @RestControllerAdvice example in Spring Boot
Or way to write Unit Test:
– Spring Boot Unit Test for JPA Repository
– Spring Boot Unit Test for Rest Controller
Happy learning! See you again.
Source Code
You can find the complete source code for this tutorial on Github.
If you want to add Pagination, please visit:
Spring Boot Thymeleaf Pagination example
File Upload:
Spring Boot Thymeleaf File Upload example
Further Reading
Fullstack CRUD App:
– Vue + Spring Boot example
– Angular 8 + Spring Boot example
– Angular 10 + Spring Boot example
– Angular 11 + Spring Boot example
– Angular 12 + Spring Boot example
– Angular 13 + Spring Boot example
– Angular 14 + Spring Boot example
– React + Spring Boot example
More Practice:
– Validate Request Body in Spring Boot
– Spring Boot with Spring Security & JWT Authentication
– Spring Boot Rest XML example – Web service with XML Response
– Spring Boot Multipart File upload example
– Spring Boot Pagination and Sorting example
Associations:
– Spring Boot One To One example with JPA, Hibernate
– Spring Boot One To Many example with JPA, Hibernate
– Spring Boot Many to Many example with JPA, Hibernate