Spring Boot CRUD Application + SQL Database + @ManyToMany + Multiple Select

What you’ll build:
A Spring Boot CRUD app with an SQL Database, @ManyToMany relation, multiple select. Initialize your DB with data and deploy your App on Heroku.

Source Codehttps://github.com/platoiscodingcom/books_crud
Starting Codehttps://github.com/platoiscodingcom/books_crud_starter
Live Versionhttps://spring-boot-books-crud-app.herokuapp.com/
Fronend Design byhttps://www.creative-tim.com/product/paper-dashboard/?affiliate_id=96916
data.sqlhttps://github.com/platoiscodingcom/books_crud/blob/master/src/main/resources/data.sql

You can download and import the Starting Code or…
(1) generate a project with Spring Initializr (Dependencies: Web, JPA, SQL, Thymeleaf, PostgreSQL(if you want to deploy on Heroku) find an example here).
(2) download the CSS files of the Fronend Design and place them in your rousources>static folder

Development Environment:

EditorIntellJ, Eclipse or one of your choice
LanguagesJava 8+, JavaScript, (HTML, CSS)
Misc.Git, SQL Workbench, Heroku CLI
AccountsGitHub (for Version Control), Heroku

Folder Structure & Database:

Create a new Database “books_crud_db” (mySQl Workbench) and change name and password in application.properties file. Open the data.sql file. If you’re working with IntelliJ you will be asked to configure your datasource. How to do that you can find here under “application.properties and data.sql.

Get the data.sql file here:

application.properties:

#########################################################
#to deploy on Heroku use this:
#########################################################
#spring.datasource.url=${JDBC_DATABSE_URL}
#spring.datasource.username=${JDBC_DATABSE_USERNAME}
#spring.datasource.password=${JDBC_DATABSE_PASSWORD}

#spring.jpa.show-sql = false
#spring.jpa.generate.ddl = true
#spring.jpa.hibernate.ddl-auto = create

#spring.datasource.initialization-mode=always

#########################################################
# ...and delete all of this:
#########################################################

spring.datasource.url=jdbc:mysql://localhost/book_crud_db?useSSL=false

#place your password and username from your MySQL Workbench
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.show-sql=true

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
##########################################################
#either:
#init data after every restart with data.sql
##########################################################
spring.jpa.hibernate.ddl-auto=create
spring.datasource.initialization-mode=always

##########################################################
#Or: do not init but continue to work with same DB content
#spring.jpa.hibernate.ddl-auto=update
##########################################################

## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG

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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-books-crud-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-books-crud-app</name>
    <description>tutorial for platoiscoding.com</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <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-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</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>

 

Backend:
In the Backend we will establish the @ManyToMqny relation between Books and their Categories. Each book can have one or more categories and each category can have one or more books.

Book and Category Entities:

Our Database will generate a new table “book_category” and insert book_ids and category_ids.

If your book with the id “10” has the categories cat_x(id: 23), cat_y(id:24) and cat_z(id:25) then the table book_category will contain the entries (10, 23), (10,24), (10,25).

FetchType.LAZY is the default fetch type and “doesn’t load the relationships unless explicitly “asked for” via getter” (source: howtoprogrammwithjava)

@Entity
@Table(name = "books")
public class Book {

    @Id
    @Column(name = "book_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotEmpty
    private String title;

    @NotEmpty
    private String author;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "book_category",
            joinColumns = { @JoinColumn(name = "book_id") },
            inverseJoinColumns = { @JoinColumn(name = "category_id") })
    private Set<Category> categories = new HashSet<>();

    @Column(name = "Year")
    @DateTimeFormat(pattern = "yyyy")
    private Date dateField;

    @Lob
    @NotEmpty
    @Type(type = "org.hibernate.type.TextType") //heroku config
    private String description;


    public Book(){}

    /*... Getter and Setter ... */

}
@Entity
@Table(name = "categories")
public class Category {
    @Id
    @Column(name ="cat_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @NotEmpty
    private String name;

    @ManyToMany(mappedBy = "categories")
    private Set<Book> books;

    public Category() {}

    /* ... Getter and Setter ... */
}

Repositories:

public interface BookRepository extends CrudRepository<Book, Long> {
}
public interface CategoryRepository extends CrudRepository<Category, Long> {
}

Service Layer:

public interface CrudService<T, ID> {
    /**
     * GET all Objects from DB
     * @return all Objects from Database
     */
    Set<T> getAll();

    /**
     * finds an Object by its ID
     * @param id    Database ID of Object
     * @return      Object
     */
    T findById(ID id);

    /**
     * creates new Object and saves it in Database
     * @param tDetails   field values
     * @return           new Object
     */
    Long create(T tDetails);

    /**
     * updates Object from Database with field values in taskDetails
     * @param id        Database ID of Object
     * @param tDetails  field values
     * @return          updated Object
     */
    void update(ID id, T tDetails);

    /**
     * deletes Object from Database
     * @param id    Database ID of Object
     */
    void delete(ID id);
}
@Service
public interface BookService extends CrudService<Book, Long> {

    Set<Book> getAll();
    Book findById(Long bookId);
    Long create(Book bookDetails);
    void update(Long bookId, Book bookDetails);
    delete(Long bookId);

}
@Service
public interface CategoryService extends CrudService<Category, Long> {

    Set<Category> getAll();
    Category findById(Long catId);
    Long create(Category categoryDetails);
    void update(Long catId, Category categoryDetails);
    void delete(Long catId);
}
@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookRepository bookRepository;

    @Override
    public Set<Book> getAll(){
        Set<Book> bookSet = new HashSet<>();
        bookRepository.findAll().iterator().forEachRemaining(bookSet::add);
        return bookSet;
    }

    @Override
    public Book findById(Long bookId){
        Optional<Book> bookOptional = bookRepository.findById(bookId);

        if (!bookOptional.isPresent()) {
            throw new RuntimeException("Book Not Found!");
        }
        return bookOptional.get();
    }

    @Override
    public void update(Long bookId, Book bookDetails){
        Book currentBook = findById(bookId);
        currentBook.setTitle(bookDetails.getTitle());
        currentBook.setAuthor(bookDetails.getAuthor());
        currentBook.setCategories(bookDetails.getCategories());
        currentBook.setDescription(bookDetails.getDescription());
        currentBook.setDateField(bookDetails.getDateField());

        bookRepository.save(currentBook);
    }

    @Override
    public void delete(Long bookId){
        bookRepository.deleteById(bookId);
    }

    @Override
    public Long create(Book bookDetails){
        bookRepository.save(bookDetails);
        return bookDetails.getId();
    }
}
@Service
public class CategoryServiceImpl implements CategoryService{

    @Autowired
    private CategoryRepository categoryRepository;

    @Override
    public Set<Category> getAll(){
        Set<Category> categorySet = new HashSet<>();
        categoryRepository.findAll().iterator().forEachRemaining(categorySet::add);
        return categorySet;
    }

    @Override
    public Category findById(Long catId){ Optional<Category> categoryOptional = categoryRepository.findById(catId);

        if (!categoryOptional.isPresent()) {
            throw new RuntimeException("Category Not Found!");
        }
        return categoryOptional.get();
    }

    @Override
    public void delete(Long catId){
        categoryRepository.deleteById(catId);
    }

    @Override
    public Long create(Category categoryDetails){
        categoryRepository.save(categoryDetails);
        return categoryDetails.getId();
    }

    @Override
    public void update(Long catId, Category categoryDetails){
        Category currentCat = findById(catId);
        currentCat.setName(categoryDetails.getName());

        categoryRepository.save(currentCat);
    }

}

BookController:

@Controller
public class BookController {

    @Autowired
    private BookService bookService;
    @Autowired
    private CategoryService categoryService;

    /**
     * Displays a single Book
     * @param id            book Id
     * @param model         book object
     * @return              template for displaying a single book
     */
    @RequestMapping( path = "/book/show/{id}")
    public String showSingleBook(@PathVariable("id") long id, Model model) {
        Book book = bookService.findById(id);
        model.addAttribute("book", book);
        return "books/showById";
    }

    /**
     * New Book Form
     * @param model     book object
     * @return          template form for new book
     */
    @RequestMapping( path = "/book/create")
    public String newBookForm(Model model) {
        model.addAttribute("book", new Book());
        Set<Category> categories = categoryService.getAll();
        model.addAttribute("categories", categories);
        return "books/new";
    }

    /**
     * saves the details of "book/create" to DB
     * @param book      field values
     * @return          redirection to list view of all books
     */
    @RequestMapping(path = "/book", method = RequestMethod.POST)
    public String saveNewBook(Book book) {
        long id = bookService.create(book);
        return "redirect:/books";
    }

    /**
     * Edit Form
     * @param id        book Id
     * @param model     book object
     * @return          template for editing a book
     */
    @GetMapping("/book/{id}")
    public String editBookForm(@PathVariable("id") long id, Model model) {
        Book book = bookService.findById(id);
        Set<Category> categories = categoryService.getAll();
        model.addAttribute("allCategories", categories);
        model.addAttribute("book", book);
        return "books/edit";
    }

    /**
     * shows all existing books in DB as list
     * @param model     book objects
     * @return          template for list view
     */
    @GetMapping({"/books", "/"})
    public String showAllBooks(Model model) {
        model.addAttribute("books", bookService.getAll());
        model.addAttribute("categories", categoryService.getAll());
        return "index";
    }

    /**
     * Saves book details from edit template for an existing book in DB
     * @param id        book Id
     * @param book      book details (of field values)
     * @return          redirection to list view of all books
     */
    @RequestMapping(path = "/book/{id}", method = RequestMethod.POST)
    public String updateBook(@PathVariable("id") long id, Book book) {
        bookService.update(id, book);
        return "redirect:/books";    }

    /**
     * deletes existing book from DB
     * @param id        book Id
     * @return          redirection to list view of all books
     */
    @RequestMapping(path = "/book/delete/{id}", method = RequestMethod.GET)
    public String deleteBook(@PathVariable("id") long id) {
        bookService.delete(id);
        return "redirect:/books";
    }
}

CategoryController:

@Controller
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * Returns Form for new Category
     * @param model     contains Category Object
     * @return          template Form for new Category
     */
    @RequestMapping( path = "/category/create")
    public String newCatForm(Model model) {
        model.addAttribute("category", new Category());
        return "categories/new";
    }

    /**
     * Save the Details of the NewCategoryForm in DD
     * @param category   contains field values
     * @return           redirection to categories list
     */
    @RequestMapping(path = "/category", method = RequestMethod.POST)
    public String saveNewCategory(Category category) {
        long id = categoryService.create(category);
        return "redirect:/categories";
    }

    /**
     * Returns an Edit Form for an existing Category
     * @param id        Id of Category
     * @param model     contains Category Object
     * @return          edit Form
     */
    @GetMapping("/category/{id}")
    public String editCategoryForm(@PathVariable("id") long id, Model model) {
        Category category = categoryService.findById(id);
        model.addAttribute("category", category);
        return "categories/edit";
    }

    /**
     * Shows Books by Category
     * @param category_id        category_id
     * @param model              contains a Category and its Books
     * @return                   list view of books
     */
    @GetMapping("/category/books/{id}")
    public String showBooksByCategory(@PathVariable("id") long category_id, Model model) {
        Category category = categoryService.findById(category_id);
        Set<Book> books = category.getBooks();
        model.addAttribute("category", category);
        model.addAttribute("books", books);
        return "categories/showById";
    }

    /**
     * List/Table view of all existing Categories in Database
     * @param model         contains Categories from DB
     * @return              list view
     */
    @GetMapping("/categories")
    public String showAllCategories(Model model) {
        model.addAttribute("categories", categoryService.getAll());
        return "categories/list";
    }

    /**
     * After clicking "save" in the edit Category Form
     * details will be directed here
     * Saves updates existing Category Object in DB
     * @param id            category Id
     * @param category      field values of edit Form
     * @return
     */
    @RequestMapping(path = "/category/{id}", method = RequestMethod.POST)
    public String updateCategory(@PathVariable("id") long id, Category category) {
        categoryService.update(id, category);
        return "redirect:/categories";    }

    /**
     * Deletes existing Object
     * @param id            Catefory Id
     * @return              redirect to list view of all Categories
     */
    @RequestMapping(path = "/category/delete/{id}", method = RequestMethod.GET)
    public String deleteCategory(@PathVariable("id") long id) {
        categoryService.delete(id);
        return "redirect:/categories";
    }
}

 

The Frontend:

index.html :

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
        <!-- Main Panel -->
        <div class="main-panel">
            <th:block th:include="fragments/navbar"></th:block>
            <!-- Begin Page Content -->
            <div class="content">
                <div class="container-fluid">
                    <div class="row">
                        <div class="col-md-12">
                            <th:block th:include="bookTemplates/showAll"></th:block>
                        </div>
                    </div>
                </div><!-- /.container-fluid -->
            </div>
            <!-- /.content -->
        </div>
        <!-- End of Main Panel -->
        <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

fragments/head.html:

<head>
    <meta charset="utf-8" />
    <link rel="apple-touch-icon" sizes="76x76" href="assets/img/apple-icon.png">
    <link rel="icon" type="image/png" sizes="96x96" href="assets/img/favicon.png">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

    <title>Books Manager</title>

    <meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
    <meta name="viewport" content="width=device-width" />


    <!-- Bootstrap core CSS     -->
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet" />

    <!-- Animation library for notifications   -->
    <link th:href="@{/css/animate.min.css}" rel="stylesheet"/>

    <!--  Paper Dashboard core CSS    -->
    <link th:href="@{/css/paper-dashboard.css}" rel="stylesheet"/>

    <!--  CSS for Demo Purpose, don't include it in your project     -->
    <link th:href="@{/css/demo.css}" rel="stylesheet" />

    <!--  Fonts and icons     -->
    <link href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css" rel="stylesheet">
    <link href='https://fonts.googleapis.com/css?family=Muli:400,300' rel='stylesheet' type='text/css'>
    <link th:href="@{/css/themify-icons.css}" rel="stylesheet">

</head>

fragments/navbar.html:

customize your toggle navigation with the links you need.

<nav class="navbar navbar-default">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar bar1"></span>
                <span class="icon-bar bar2"></span>
                <span class="icon-bar bar3"></span>
            </button>
            <a class="navbar-brand" href="#"></a>
        </div>
    </div>
</nav>

fragments/sidebar.html:

<div class="sidebar" data-background-color="white" data-active-color="danger">

    <!--
        Tip 1: you can change the color of the sidebar's background using: data-background-color="white | black"
        Tip 2: you can change the color of the active button using the data-active-color="primary | info | success | warning | danger"
    -->

    <div class="sidebar-wrapper">
        <div class="logo">
            <a th:href="@{/books}" class="simple-text">
                Books Manager
            </a>
        </div>

        <ul class="nav">
            <li>
                <a th:href="@{/books}">
                    <i class="ti-book"></i>
                    <p>Books</p>
                </a>
            </li>
            <li>
                <a th:href="@{/book/create}">
                    <i class="ti-plus"></i>
                    <p>Add Book</p>
                </a>
            </li>
            <li>
                <a th:href="@{/categories}">
                    <i class="ti-folder"></i>
                    <p>Categories</p>
                </a>
            </li>
            <li>
                <a th:href="@{/category/create}">
                    <i class="ti-plus"></i>
                    <p>Add Category</p>
                </a>
            </li>
        </ul>
    </div>
</div>

fragments/footer.html:

<footer class="footer">
    <div class="container-fluid">
        <nav class="pull-left">
            <ul>

                <li>
                    <a href="http://www.creative-tim.com">
                        Creative Tim
                    </a>
                </li>
                <li>
                    <a href="http://blog.creative-tim.com">
                        Blog
                    </a>
                </li>
                <li>
                    <a href="http://www.creative-tim.com/license">
                        Licenses
                    </a>
                </li>
            </ul>
        </nav>
        <div class="copyright pull-right">
            &copy; <script>document.write(new Date().getFullYear())</script>, made with <i class="fa fa-heart heart"></i> by <a href="http://www.creative-tim.com">Creative Tim</a>
        </div>
    </div>
</footer>

fragments/scripts.html:

<!--   Core JS Files   -->
<script th:src="@{/js/jquery.min.js}" type="text/javascript"></script>
<script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>

<!--  Checkbox, Radio & Switch Plugins -->
<script th:src="@{/js/bootstrap-checkbox-radio.js}"></script>

<!--  Charts Plugin -->
<script th:src="@{/js/chartist.min.js}"></script>

<!--  Notifications Plugin    -->
<script th:src="@{/js/bootstrap-notify.js}"></script>

<!--  Google Maps Plugin    -->
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>

<!-- Paper Dashboard Core javascript and methods for Demo purpose -->
<script th:src="@{/js/paper-dashboard.js}"></script>

<!-- Paper Dashboard DEMO methods, don't include it in your project! -->
<script th:src="@{/js/demo.js}"></script>

 

CRUD Thymeleaf Templates for books:

books/edit.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="bookTemplates/editBook"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

books/new.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="bookTemplates/newBook"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

books/list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="bookTemplates/showAll"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

books/showById.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="bookTemplates/showSingle"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

bookTemplates/editBook.html:

<div class="card">
    <div class="header">
        <h4 class="title">Edit Book</h4>
        <p class="category">fill out & click save</p>
    </div>

    <div class="content">
        <form th:object="${book}" th:action="@{/book/{id}(id=${book.id})}" th:method="post">
            <div class="form-group">
                <input type="hidden" class="form-control" th:field="*{id}"/>
            </div>
            <div class="form-group">
                <label for="title">Title</label>
                <input type="text" class="form-control" id="title" th:field="*{title}">
            </div>
            <div class="form-group">
                <label for="author">Author</label>
                <input type="text" class="form-control" id="author" th:field="*{author}"/>
            </div>
            <div class="form-group">
                <label for="date">Year</label>
                <input type="text" class="form-control" id="date" th:field="*{dateField}"/>
            </div>
            <div class="form-group">
                <label for="category">Category</label>
                <select id="category" class="form-control" th:field="*{categories}" multiple="multiple">
                    <option th:each="category : ${allCategories}"
                            th:text="${category.name}"
                            th:value="${category.id}"
                            th:selected="${book.categories.contains(category)}"></option>
                </select>
            </div>
            <div class="form-group">
                <label for="description">Desciption</label>
                <textarea rows="4" type="textarea" class="form-control" id="description" th:field="*{description}"/></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
            <a th:href="@{'/books'}" class="btn btn-default">Cancel</a>
        </form>

    </div>
</div>

bookTemplates/newBook.html:

<div class="card">
    <div class="header">
        <h4 class="title">Add new Book</h4>
        <p class="category">fill out & click save</p>
    </div>

    <div class="content">
        <form th:object="${book}" th:action="@{/book}" th:method="post">
            <div class="form-group">
                <input type="hidden" class="form-control" th:field="*{id}"/>
            </div>
            <div class="form-group">
                <label for="title">Title</label>
                <input type="text" class="form-control" id="title" aria-describedby="title" th:field="*{title}" playeholder="Harry Potter and the Deathly Hollows">
            </div>
            <div class="form-group">
                <label for="author">Author</label>
                <input type="text" class="form-control" id="author" aria-describedby="author" th:field="*{author}" playeholder="Joanne K. Rowling"/>
            </div>
            <div class="form-group">
                <label for="category">Category</label>
                <select id="category" class="form-control" th:field="*{categories}" multiple="multiple">
                    <option th:each="category : ${categories}" th:text="${category.name}" th:value="${category.id}"></option>
                </select>
            </div>
            <div class="form-group">
                <label for="date">Year</label>
                <input type="text" class="form-control" id="date" th:field="*{dateField}"/>
            </div>
            <div class="form-group">
                <label for="description">Description</label>
                <textarea rows="4" type="textarea" class="form-control" id="description" th:field="*{description}" playeholder="type here ..."/></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>

    </div>
</div>

bookTemplates/showAll.html:

<div class="card">
    <div class="header">
        <h4 class="title">Books</h4>
        <p class="category">shows all available books</p>
    </div>

    <div class="content table-responsive table-full-width">
        <table class="table table-striped">
            <thead>
            <th>ID</th>
            <th>Title</th>
            <th>Author</th>
            <th>Category</th>
            <th></th>
            <th></th>
            <th></th>
            </thead>
            <tbody>
            <tr th:each="book : ${books}">
                <td th:text="${book.id}"></td>
                <td th:text="${book.title}"></td>
                <td th:text="${book.author}"></td>
                <td>
                    <th:block th:each="category : ${book.categories}">
                        <span th:text="${category.name} + ' '">Item description here...</span>
                    </th:block>
                </td>
                <td><a th:href="@{'/book/show/' + ${book.id}}" class="btn btn-primary">View</a></td>
                <td><a th:href="@{'/book/' + ${book.id}}" class="btn btn-default">Edit</a></td>
                <td><a th:href="@{'/book/delete/' + ${book.id}}" class="btn btn-danger">Delete</a></td>
            </tr>
            </tbody>
        </table>

    </div>
</div>

bookTemplates/showSingle.html:

<div class="card">
    <div class="header">
        <h4 th:text="${book.title}" class="title">Edit Book</h4>
        <p class="category">Details</p>
    </div>

    <div class="content">
        <p th:text="${book.author}"></p>
        <p th:text="${#dates.format(book.dateField, 'yyyy')}"></p>
        <p th:text="${book.description}"></p>
        <th:block th:each="category : ${book.categories}">
            <p th:text="${category.name}"></p>
        </th:block>
    </div>
</div>

 

CRUD Thymeleaf Templates for categories:

categories/new.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="categoryTemplates/newCat"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

categories/edit.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="categoryTemplates/editCat"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

categories/list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="categoryTemplates/showAllCat"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

categories/showById.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>
<!-- Page Wrapper -->
<div id="wrapper">
    <th:block th:include="fragments/sidebar"></th:block>
    <!-- Main Panel -->
    <div class="main-panel">
        <th:block th:include="fragments/navbar"></th:block>
        <!-- Begin Page Content -->
        <div class="content">
            <div class="container-fluid">
                <div class="row">
                    <div class="col-md-12">
                        <th:block th:include="categoryTemplates/booksByCat"></th:block>
                    </div>
                </div>
            </div><!-- /.container-fluid -->
        </div>
        <!-- /.content -->
    </div>
    <!-- End of Main Panel -->
    <th:block th:include="fragments/footer"></th:block>
</div>

<th:block th:include="fragments/scripts"></th:block>
</body>
</html>

categoryTemplates/booksByCat.html

<div class="card">
    <div class="header">
        <h4 class="title">Books for <span th:text="${category.name}"></span></h4>
        <p class="category">shows all available books</p>
    </div>

    <div class="content table-responsive table-full-width">
        <table class="table table-striped">
            <thead>
            <th>ID</th>
            <th>Title</th>
            <th>Author</th>
            <th></th>
            <th></th>
            <th></th>
            </thead>
            <tbody>
            <tr th:each="book : ${books}">
                <td th:text="${book.id}"></td>
                <td th:text="${book.title}"></td>
                <td th:text="${book.author}"></td>
                <td><a th:href="@{'/book/show/' + ${book.id}}" class="btn btn-primary">View</a></td>
                <td><a th:href="@{'/book/' + ${book.id}}" class="btn btn-default">Edit</a></td>
                <td><a th:href="@{'/book/delete/' + ${book.id}}" class="btn btn-danger">Delete</a></td>
            </tr>
            </tbody>
        </table>

    </div>
</div>

categoryTemplates/editCat.html

<div class="card">
    <div class="header">
        <h4 class="title">Edit Category</h4>
        <p class="category">fill out & click save</p>
    </div>

    <div class="content">
        <form th:object="${category}" th:action="@{/category/{id}(id=${category.id})}" th:method="post">
            <div class="form-group">
                <input type="hidden" class="form-control" th:field="*{id}"/>
            </div>
            <div class="form-group">
                <label for="name">Author</label>
                <input type="text" class="form-control" id="name" aria-describedby="name" th:field="*{name}"/>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
            <a th:href="@{'/categories'}" class="btn btn-default">Cancel</a>
        </form>

    </div>
</div>

categoryTemplates/newCat.html

<div class="card">
    <div class="header">
        <h4 class="title">Add new Category</h4>
        <p class="category">fill out & click save</p>
    </div>

    <div class="content">
        <form th:object="${category}" th:action="@{/category}" th:method="post">
            <div class="form-group">
                <input type="hidden" class="form-control" th:field="*{id}"/>
            </div>
            <div class="form-group">
                <label for="name">Name</label>
                <input type="text" class="form-control" id="name" aria-describedby="name" th:field="*{name}" placeholder="type here ..."/>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>

    </div>
</div>

categoryTemplates/showAllCat.html

<div class="card">
    <div class="header">
        <h4 class="title">Categories</h4>
        <p class="category">shows all Categories</p>
    </div>

    <div class="content table-responsive table-full-width">
        <table class="table table-striped">
            <thead>
            <th>ID</th>
            <th>Name</th>
            <th></th>
            <th></th>
            </thead>
            <tbody>
            <tr th:remove="all">
                <td>123</td>
                <td>Fantasy</td>
                <td></td>
                <td></td>
            </tr>
            <tr th:each="category : ${categories}">
                <td th:text="${category.id}"></td>
                <td th:text="${category.name}"></td>
                <td><a th:href="@{'/category/books/' + ${category.id}}" class="btn btn-primary">View</a></td>
                <td><a th:href="@{'/category/' + ${category.id}}" class="btn btn-default">Edit</a></td>
                <td><a th:href="@{'/category/delete/' + ${category.id}}" class="btn btn-danger">Delete</a></td>
            </tr>
            </tbody>
        </table>

    </div>
</div>

This concludes the Frontend. Run the app with “mvn:spring-boot:run” in your terminal.

Deploy your App to Heroku:

application.properties.file: Delete all and place the following

## Heroku Properties
spring.datasource.url=${JDBC_DATABSE_URL}
spring.datasource.username=${JDBC_DATABSE_USERNAME}
spring.datasource.password=${JDBC_DATABSE_PASSWORD}

spring.jpa.show-sql = false
spring.jpa.generate.ddl = true
spring.jpa.hibernate.ddl-auto = create
        
spring.datasource.initialization-mode=always

Next open the command line runner and navigate to your application folder

(1) enter: “heroku login” and provide your email and password (you need to have an account on Heroku by now)
(2) then: “git init” and “git add .” which will add all your files to the version control
(3) git commit -m “your commit message”
(4) heroku create “name of your app”
(5) git push heroku master
(6) heroku open – will open your deployed app in your browser

Leave a Reply