Spring Boot Thymeleaf CRUD example

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

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:

spring-boot-thymeleaf-crud-example-create

– Retrieve all objects:

spring-boot-thymeleaf-crud-example-retrieve

– Update status of an item:

spring-boot-thymeleaf-crud-example-update-status

– Update/Edit an object in its own page:

spring-boot-thymeleaf-crud-example-update

– Search Tutorials by title:

spring-boot-thymeleaf-crud-example-search

– Delete an item:

spring-boot-thymeleaf-crud-example-delete

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

spring-boot-thymeleaf-crud-example-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 or PostgreSQLDialect for PostgreSQL
  • spring.jpa.hibernate.ddl-auto is used for database initialization. We set the value to update 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 be validate.

– 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 and jdbc: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 url http://localhost:8080/h2-console will change to http://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:

spring-boot-thymeleaf-crud-example-template

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