Spring Boot CRUD Tutorial with embedded H2 and Freemarker

Tutorial describing how to create the simple CRUD (Create Read Update Delete) application using Spring Boot, Freemarker template engine and H2 as embedded database.

Technologies used:

  1. Spring Boot 2.1.7.RELEASE
  2. JDK 1.8
  3. Maven 3
  4. Freemarker
  5. JQuery
  6. Spring Data JPA
  7. H2 database

Project setup

Project dependencies managed by the Maven’s pom.xml configuration file:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.devcases.springboot</groupId>
    <artifactId>crud-freemarker</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <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-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

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

</project>

The explanation of above dependencies:

  • spring-boot-starter-web – packages all the necessary dependencies together with the auto configuration for running simple web application
  • spring-boot-starter-tomcat – provides the embedded tomcat application server
  • spring-boot-starter-data-jpa – provides auto configuration for using Spring Data JPA with Hibernate
  • spring-boot-starter-freemarker – provides auto configuration for Freemarker templates
  • h2 – provides auto configured embedded H2 database
  • spring-boot-maven-plugin – plugin prividing commands to work with a Spring Boot application (we will be usine the spring-boot:run command)

Application Initializer

@SpringBootApplication
@ComponentScan(basePackages={"com.devcases.springboot.crud.library"})
@EnableJpaRepositories(basePackages="com.devcases.springboot.crud.library.repository")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

The @EnableJpaRepositories annotation tells Spring to instantiate Spring Data repositories defined in the specified directory.

Controller

@Controller
public class LibraryController {

private BookService service;

@Autowired
public LibraryController(BookService service) {
this.service = service;
}

@GetMapping("/books")
public String showAllBooks(Model model) {
model.addAttribute("books", service.findAll());
return "books";
}

@GetMapping("/new-book")
public String showBookCreationForm(Model model) {
model.addAttribute("book", new Book());
return "new-book";
}

@PostMapping(value = "/add", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String addNewBook(@Valid @ModelAttribute Book book, BindingResult result, Model model) {
if (result.hasErrors()) {
return "new-book";
}
service.save(book);
model.addAttribute("books", service.findAll());
return "redirect:/books";
}

@GetMapping("/{id}")
public String showBookdById(@PathVariable Long id, Model model) {
Book book = service.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + id));
model.addAttribute("book", book);
return "edit-book";
}

@PostMapping("/{id}/update")
public String updateBook(@PathVariable Long id, @Valid @ModelAttribute Book book, BindingResult result, Model model) {
if (result.hasErrors()) {
return "edit-book";
}
service.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + id));
service.save(book);
model.addAttribute("books", service.findAll());
return "redirect:/books";
}

@DeleteMapping("/{id}")
public String deleteBook(@PathVariable Long id, Model model) {
service.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + id));
service.deleteById(id);
model.addAttribute("books", service.findAll());
return "books";
}
}

The controller contains set of methods repsonsible for CRUD operations:

  • showAllBooks – finds and shows all the persisted books in the “books” view
  • showBookCreationForm – creates empty book and allows user to fill the book in the “new-book” view
  • addNewBook – fired when data of new book are requested to persist. Thanks to the @Valid annotation, the Spring validates (according to the validation annotations in the Book class) passed book and provides errors in the BindingResult object. Based on the result we can return to the same view with the errors or save the book in the database. After successfull operation method shows the “books” view
  • showBookdById – finds the book with the specified id and shows its data in the “edit-book” view.
  • updateBook – saves existing book (with the id passed in the url – we use PathVariable annotation to resolve this id), validates data in the same way as the showBookCreationForm method.
  • deleteBook – deletes book with specified id.

Service

@Service
public class BookService {

private BookRepository repository;

@Autowired
public BookService(BookRepository repository) {
this.repository = repository;
}

public List<Book> findAll() {
return StreamSupport.stream(repository.findAll().spliterator(), false)
.collect(Collectors.toList());
}

public Optional<Book> findById(Long id) {
return repository.findById(id);
}

public Book save(Book stock) {
return repository.save(stock);
}

public void deleteById(Long id) {
repository.deleteById(id);
}
}

Repository

@Repository
public interface BookRepository extends CrudRepository<Book, Long> {
}

Using the Spring Data @Repository annotation and CrudReposittory interface we get the generated implementation that provides us CRUD methods to manage Book instances.

Model

@Entity
public class Book {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

@NotBlank(message = "Author is mandatory")
private String author;

@NotBlank(message = "Name is mandatory")
private String name;

public long getId() {
return id;
}

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

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getName() {
return name;
}

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

View

books.ftl template:

<#import "/spring.ftl" as spring />
<html>
<head>
<title>Book Library</title>
<script type="text/javascript" src="js/jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</head>
<body>
<div>
<h2>Library</h2>
<hr/>
<a href="/new-book">
<button type="submit">Add new book</button>
</a>
<br/><br/>
<div>
<div>
<div>Book list</div>
</div>
<div>
<table>
<tr>
<th>Id</th>
<th>Author</th>
<th>Name</th>
</tr>
<#if (books?? && books?size != 0)>
<#list books as book>
<tr>
<td>${book.id}</td>
<td>${book.author}</td>
<td>${book.name}</td>
<td>
<a href="/${book.id}">Edit</a>
</td>
<td>
<button id="deleteBookButton" onclick="deleteBook(${book.id})">Delete</button>
<!--<button id="deleteBookButton" onclick="test(this.id) bookId="${book.id}">Delete</button>-->
</td>
</tr>
</#list>
<#else>
No books in library
</#if>
</table>
</div>
</div>
</div>
</body>
</html>

new-book.ftl template:

<#import "spring.ftl" as spring />
<!DOCTYPE html>
<html lang="en">
<head>
<title>Book Library</title>
</head>
<body>
<div>
<h2>New User</h2>
<div>
<div>
<@spring.bind "book"/>
<form action="/add" method="post">
<div>
<div>
Author:
<@spring.formInput "book.author"/>
<@spring.showErrors "<br>"/>
</div>
<div>
Name:
<@spring.formInput "book.name"/>
<@spring.showErrors "<br>"/>
</div>
</div>
<div>
<div>
<input type="submit" value="Add User">
</div>
</div>
</form:form>
</div>
</div>
</div>
</body>
</html>

edit-book.ftl template:

<#import "spring.ftl" as spring />
<!DOCTYPE html>
<html lang="en">
<head>
<title>Book Library</title>
</head>
<body>
<div>
<h2>New User</h2>
<div>
<div>
<@spring.bind "book"/>
<form action="/${book.id}/update" method="post">
<div>
<div>
Id: ${book.id}
</div>
<div>
Author:
<@spring.formInput "book.author"/>
<@spring.showErrors "<br>"/>
</div>
<div>
Name:
<@spring.formInput "book.name"/>
<@spring.showErrors "<br>"/>
</div>
</div>
<div>
<div>
<input type="submit" value="Update User">
</div>
</div>
</form:form>
</div>
</div>
</div>
</body>
</html>

main.js with jquery code to perform delete request:

function deleteBook(bookId) {
$.ajax({
url: '/' + bookId,
type: 'DELETE',
success: function(result) {
var newDoc = document.open("text/html", "replace");
newDoc.write(result);
newDoc.close();
}
});
}

Configuration

The configuration in application.properties file contains information how to resolve Freemarker files. The templates (with suffix .ftl) are located under resources the resources/ftl directory:

spring.freemarker.template-loader-path: classpath:/ftl
spring.freemarker.suffix: .ftl

Running

The project can be started by the running the following command:

mvn clean spring-boot:run

The command deploys the apllication on the embedded tomcat server.

By visiting the localhost:8080/books url, we should see the main page containing empty list of books with the ability to add new ones. Because we used the in memory H2 database, restart of the application resets the database – all our data is lost.

The example code you can download from here

Leave a Reply

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