Simple Spring Boot CRUD application with Thymeleaf, JPA and Bootstrap

Published by Alexander Braun on 14 Dec 2017 - tagged with Spring, Spring Boot, Bootstrap, Thymeleaf

In this post, I will describe a full working example of a simple Spring Boot based CRUD application. The example code uses Thymeleaf Template Engine, Bootstrap 3 and Spring JPA in conjunction with HSQL DB.

Introduction

The idea of this post is to create and describe a basic Spring Boot CRUD application that can be extended and can be used as a starter template for more complex projects. At this point in time important features like server and client-side validation, pagination and sorting are missing. These additional features will be added in future articles.

What does the application do?

To keep things as simple as possible the use case for this application is to provide an application that allows you to create, read, update and delete notes. The notes object only consists of the following attributes:

  • id: the unique id of a notes object
  • title: the title of the notes
  • content: the actual content of the notes

Typical for a basic CRUD application is that the application iself consists of two views:

  • Table view: shows all existing notes objects and provides access to all CRUD operations
  • Create/Edit view: used to create a new object or edit an existing object

Where is the code?

The code for this post is available in this GitHub repository

Implementation

Maven dependencies

As described above we need the following artifacts for the application:

  • spring-boot-starter-web
  • spring-boot-starter-thymeleaf
  • spring-boot-starter-data-jpa
  • hsqldb

The entire pom.xml file is shown below

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.progressive.code</groupId>
    <artifactId>CrudDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>${project.artifactId}</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Main Class

Now we can add the main class for the application. In this case the main class CrudApp is quite simple. We only have to add the @SpringBootApplication annotation which is a convenient shortcut for these Spring annotations @Configuration, @EnableAutoConfiguration and @ComponentScan.

CrudApp.java

package com.progressive.code.crud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Created by abraun on 10/11/2017.
 */
@SpringBootApplication
public class CrudApp {

    public static void main(String[] args) {
        SpringApplication.run(CrudApp.class, args);
    }

}

Domain object

Next, we create the Notes domain object. Besides the usual constructors and getter and setter methods, the annotations required to describe the object as a database entity will be added as well.

Notes.java

package com.progressive.code.crud.domain;

import javax.persistence.*;

/**
 * Created by abraun on 23/11/2017.
 */
@Entity
public class Notes {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    @Column(length=10000)
    private String content;

    public Notes() {
    }

    public Notes(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Let's talk about the annotations from the javax.persistence package:

  • @Entity: Declares the domain class as an entity - basically an object we want to store in the database
  • @Id: The id attribute has been annotated with @Id to specify this attribute as the primary key in the database
  • @GeneratedValue(strategy = GenerationType.AUTO): Whenever we create a new object in the database the id should be autogenerated (e.g. sequence number)
  • @Column(length=10000): The @Column attribute is used to describe additional constraints and configurations. For the content attribute, we want a maximum length of 10,000 characters.

Persistence layer (JPA)

After describing the domain object, we have to implement the persistence layer of our application. When using Spring Boot and Spring Boot JPA, this task is extremely simple - we only have to create an interface that extends Spring's JpaRepository and annotate the class with @Repository. As we have added the dependency to our HSQL database in the pom.xml file and as this is the only available JDBC driver at runtime, Spring assumes automatically that we want to use HSQL as our persistence storage. In this case, no additional database configuration is required!

NotesRepository.java

package com.progressive.code.crud.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.progressive.code.crud.domain.Notes;

/**
 * Created by abraun on 23/11/2017.
 */
@Repository
public interface NotesRepository extends JpaRepository<Notes, Long> {
}

The @Repository stereotype annotation declares that the interface is used to specify all methods used to create, read (search), update and delete an entity within the database. As long as only "standard methods" are required the JpaRepository already declares all methods we need for this CRUD example. If additional, entity specific methods are required we could add them here (e.g. findByTitle).

Additionally, we don't even have to implement the interface as Spring Data is able to provide the required implementation out of the box. Or in other words, creating an interface, as shown above, is all we have to do to store, retrieve and modify our domain object!

The JpaRepository interface uses Generics to specify the entity and the corresponding class used for the ID column. The syntax is JpaRepository<Entity, ID>, in our case JpaRepository<Notes, Long>.

Service layer

It is possible to autowire the NotesRepository into your controller class directly and invoke the CRUD operations from the Controller, but I personally consider this as a bad practice. From my point of view, it is a better approach to create a service layer instead of having a direct dependency on the persistence layer.

Let's first create the interface with all methods the service is going to offer. In our simple case we only need methods to:

  • Retrieve all Notes objects for the table view
  • Retrieve a Notes object by its ID for the edit view
  • Save a Notes object based on the create/edit view (the same operation can be used for creating and updating)
  • Delete a Notes object from the database.

NotesService.java

package com.progressive.code.crud.service;

import java.util.List;

import com.progressive.code.crud.domain.Notes;

/**
 * Created by abraun on 23/11/2017.
 */
public interface NotesService {

    ListList<Notes> findAll();

    Notes findOne(Long id);

    Notes saveNotes(Notes notes);

    void deleteNotes(Long id);

}

Now we can implement the service class. As you can see the service class is quite easy to implement. We simply invoke the corresponding methods from the NotesRepository. The NotesRepository is being injected using Spring @Autowired annotation. Additionally, we use the @Service annotation to be able to inject the service class into the Controller class (see below).

NotesServiceImpl.java

package com.progressive.code.crud.service;

import com.progressive.code.crud.dao.NotesRepository;
import com.progressive.code.crud.domain.Notes;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Created by abraun on 23/11/2017.
 */
@Service
public class NotesServiceImpl implements NotesService {

    @Autowired
    private NotesRepository notesRepository;

    @Override
    public List<Notes> findAll() {
        return notesRepository.findAll();
    }

    @Override
    public Notes findOne(Long id) {
        return notesRepository.findOne(id);
    }

    @Override
    public Notes saveNotes(Notes notes) {
        return notesRepository.save(notes);
    }

    @Override
    public void deleteNotes(Long id) {
        notesRepository.delete(id);
    }
}

To initialize some test data we add another service class named InitApplicationService. The initializeTestData method uses the @EventListener(ApplicationReadyEvent.class) annotation to indicate that the method has to be inoked after the application has been started up. The notesService used for storing the test data is being injected and the class itself is annotated with @Service.

InitApplicationService.java

package com.progressive.code.crud.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

import com.progressive.code.crud.domain.Notes;

/**
 * Created by abraun on 23/11/2017.
 */
@Service
public class InitApplicationService {

    private static final Logger LOGGER = LoggerFactory.getLogger(InitApplicationService.class);

    @Autowired
    NotesService notesService;

    @EventListener(ApplicationReadyEvent.class)
    public void initializeTestData() {
        LOGGER.info("Initialize test data");

        notesService.saveNotes(new Notes("Test 1", "Content 1"));
        notesService.saveNotes(new Notes("Test 2", "Content 2"));

        LOGGER.info("Initialization completed");
    }

}

Controller layer

The controller layer is used to map the application URLs to the functionality to be executed, to provide the data to the view layer and to specify which page should show up. To declare a class as a controller we have to use the @Controller annotation. Additionally, we inject the service bean into the controller using @Autowired.

The first method "notesList" uses the notesService findAll method to retrieve the list of existing Notes objects from the database and pushes the result into the provided Model. The method returns the name of the view "notesList" to be used to display the list.

The "notesEditForm" method is used to populate the form to create or edit a notes object. The RequestMethod is being set to get to distinguish between showing the form (GET) and processing the form data (POST). If the URL includes a path variable id (mapping /notesEdit/{id}) we read the notes object associated with this id from the database otherwise a new notes object will be instantiated. The nodes object is being pushed to the Model to be used it in the view layer.

The "notesEdit" method is being invoked when the form is being submitted. Interestingly, we don't have to provide any kind of mapping from the form to the actual notes object. This mapping is automatically being performed by Spring in conjunction with Thymeleaf. We only have to put the notes object as part of the method signature notesEdit(Model model, Notes notes). The method stores the new or updates the existing object and returns the notesList view.

And finally, we have the "notesDelete" method that expects the following URL pattern /notesDelete/{id}. Again, we use the PathVariable annotation to extract the id from the URL and invoke the deleteNotes method from the service class.

package com.progressive.code.crud.controller;

import com.progressive.code.crud.domain.Notes;
import com.progressive.code.crud.service.NotesService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

/**
 * Created by abraun on 10/11/2017.
 */
@Controller
public class DemoController {

    @Autowired
    NotesService notesService;

    @RequestMapping(value="/")
    public String notesList(Model model) {
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

    @RequestMapping(value={"/notesEdit","/notesEdit/{id}"}, method = RequestMethod.GET)
    public String notesEditForm(Model model, @PathVariable(required = false, name = "id") Long id) {
        if (null != id) {
            model.addAttribute("notes", notesService.findOne(id));
        } else {
            model.addAttribute("notes", new Notes());
        }
        return "notesEdit";
    }

    @RequestMapping(value="/notesEdit", method = RequestMethod.POST)
    public String notesEdit(Model model, Notes notes) {
        notesService.saveNotes(notes);
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

    @RequestMapping(value="/notesDelete/{id}", method = RequestMethod.GET)
    public String notesDelete(Model model, @PathVariable(required = true, name = "id") Long id) {
        notesService.deleteNotes(id);
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

}

View layer

The final step is to create the HTML views. As mentioned above we use Thymeleaf as a template engine and Bootstrap as the UI framework to enhance the look and feel. As most of the code below is standard HTML and Thymeleaf code I will concentrate on the CRUD related parts.

As the name indicates the purpose of the first view "notesList.html" is to show the list of existing notes objects and provide the links to create, update and delete the data. We use Thymeleaf's th:each attribute to iterate over the list of existing notes objects <tr th:each="notes : ${notesList}">. For each object we create a table row and fill the columns based on the notes object data, e.g. <td th:text="${notes.id}">1</td>.

For the task column we have to use Thymeleaf's pre-processing syntax __${}__ to retrieve the id of the notes object before we create the link to edit or delete the notes object.

<a href="/notesEdit/1" th:href="@{/notesEdit/__${notes.id}__}">Edit</a>

The final html file is shown below.

notesList.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Notes</title>

    <link rel="stylesheet" type="text/css" media="all" href="../../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />

    <script type="text/javascript" th:src="@{/js/jquery.min.js}" src="js/jquery.min.js"></script>
    <script type="text/javascript" th:src="@{/js/bootstrap.min.js}" src="js/bootstrap.min.js"></script>
</head>
<body>

    <div class="container">

        <h1>Notes</h1>

        <p>
            <a href="/notesEdit" th:href="@{/notesEdit}">Create</a>
        </p>

        <table class="table">
            <thead>
                <tr>
                    <th>Id</th>
                    <th>Title</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="notes : ${notesList}">
                    <td th:text="${notes.id}">1</td>
                    <td th:text="${notes.title}">Test</td>
                    <td>
                        <a href="/notesEdit/1" th:href="@{/notesEdit/__${notes.id}__}">Edit</a>
                        <a href="/notesDelete/1" th:href="@{/notesDelete/__${notes.id}__}">Delete</a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

In the next step we create the form to create and update notes objects. Again, most of the code is standard HTML and Bootstrap markup. There are couple of interesting parts related to the CRUD application itself.

To distinguish between creating a new and updating an existing notes object a hidden form element has been added to the form. For an existing notes object the id is set, otherwise the value is empty. We use the th:field attribute to specify which attribute from the notes object we want to use as value.

<input th:type="hidden" name="id" th:field="${notes.id}" />

The other form elements (title and content) are handled in a similar way. We only have to use th:field to map which form element is used for which attribute from the notes object. This allows Spring and Thymeleaf to automatically map the form elements to the corresponding notes object attributes in the backend.

The final html file is shown below.

notesEdit.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Notes</title>

    <link rel="stylesheet" type="text/css" media="all" href="../../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />

    <script type="text/javascript" th:src="@{/js/jquery.min.js}" src="js/jquery.min.js"></script>
    <script type="text/javascript" th:src="@{/js/bootstrap.min.js}" src="js/bootstrap.min.js"></script>
</head>
<body>
    <div class="container">

        <h1>Notes</h1>

        <form class="form-group" action="/notesEdit" th:action="@{/notesEdit}" method="post">
            <input th:type="hidden" name="id" th:field="${notes.id}" />
            <div class="form-group">
                <label for="title">Title</label>
                <input type="text" class="form-control" id="title" th:field="${notes.title}" />
            </div>
            <div class="form-group">
                <label for="content">Content</label>
                <textarea class="form-control" id="content" th:field="${notes.content}"></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
            <a href="#" th:href="@{/}" class="btn btn-danger" role="button">Cancel</a>
        </form>

    </div>

</body>
</html>

Start and test the application

After following all steps above we have a fully working simple CRUD application. You can start the application either from the IDE or from the command line.

Command Line

We can use

mvn package

to build the application from the command line and

java -jar target/CrudDemo-1.0-SNAPSHOT.jar

to start the application.

Test the application

After starting up you can access the application using this URL: localhost:8090

Code on GitHub

The code for this post is available in this GitHub repository