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.
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:
Editor | IntellJ, Eclipse or one of your choice |
Languages | Java 8+, JavaScript, (HTML, CSS) |
Misc. | Git, SQL Workbench, Heroku CLI |
Accounts | GitHub (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"> © <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