Donny's Programming Blog

Making an SSR Web App with Spring Boot (Part 5)

August 04, 2019

Introduction

When we last left off, our application was had pages that would display all entities and single entities respectively. We’ve handled relating a single book to a single author. The problem we face now is that an Author can write more than one Book and a Book can have more than one Author. How do we deal with that?


Many-to-Many

This is what is known as a Many-to-Many relationship. What’s nice for us is that JPA makes this process simple. Let’s take a look at how we can implement this behavior. Let’s start with our models.


Models

Book

package com.donhamiltoniii.literateoctodollop.models;

import java.util.ArrayList;
import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Book {

	@Id
	@GeneratedValue
	private Long id;
	private String title;

	@ManyToMany
	private Collection<Author> authors;
	private String genre;

	// This is a hook for JPA - We will never call this Constructor
	public Book() {
	}

	public Book(String title, String genre, Author... authors) {
		this.title = title;
		this.authors = new ArrayList<>();
		this.genre = genre;

		for (Author author : authors) {
			this.addAuthor(author);
		}
	}

	public Long getId() {
		return id;
	}

	public String getTitle() {
		return title;
	}

	public Collection<Author> getAuthors() {
		return authors;
	}

	public String getGenre() {
		return genre;
	}

	public void addAuthor(Author author) {
		this.authors.add(author);
	}

	@Override
	public String toString() {
		return "Book [id=" + id + ", title=" + title + ", authors=" + authors + ", genre=" + genre + "]";
	}

}

So we’re changing what the type of authors is now as well as renaming it to be plural. We’re also changing our constructor so that it accepts a variable number of authors. This means we can include no Authors in the constructor or 100. First, we’re initializing this.authors to a new ArrayList. We’re then looping through all of the included authors (if any) and adding them to the authors Collection. Note that we’ve also created a new addAuthor method to our Book Class.

Author

package com.donhamiltoniii.literateoctodollop.models;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Author {

	@Id
	@GeneratedValue
	private Long id;

	private String firstName;
	private String lastName;

	@ManyToMany(mappedBy = "authors")
	private Collection<Book> books;

	public Author() {
	}

	public Author(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public Long getId() {
		return id;
	}

	public String getFirstName() {
		return firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public Collection<Book> getBooks() {
		return books;
	}

	@Override
	public String toString() {
		return "Author [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", books=" + books + "]";
	}

}

Not as much changes in Author since we already had a One-toMany relationship there.


Initializer

Everything is as it should be now except we have errors in our Initializer since we changed the format of the Book constructor. Let’s fix those.

package com.donhamiltoniii.literateoctodollop;

import javax.annotation.Resource;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;

import com.donhamiltoniii.literateoctodollop.models.Author;
import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.AuthorRepository;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Service
public class Initializer implements CommandLineRunner {

	@Resource
	AuthorRepository authorRepo;

	@Resource
	BookRepository bookRepo;

	@Override
	public void run(String... args) throws Exception {
		Author rodJohnson = authorRepo.save(new Author("Rod", "Johnson"));
		Author craigWalls = authorRepo.save(new Author("Craig", "Walls"));
		Author elisabethRobson = authorRepo.save(new Author("Elisabeth", "Robson"));

		bookRepo.save(new Book("J2EE Development without EJB", "Tech", rodJohnson));
		bookRepo.save(new Book("Spring Boot In Action", "Tech", craigWalls));
		bookRepo.save(new Book("Head First Design Patterns", "Tech", elisabethRobson));
	}

}

Really all we’re doing here is adding the Author to the end of the constructor instead of the middle. That’s because we’re now accepting a variable number of Authors in our Book constructor. Since we don’t know how many we will potentially have, that needs to come last. Let’s revisit our Initializer in just a second as we also have errors in our BookController now.


BookController

We’re having the same problem in our BookController as we did in our Initializer. Let’s make the changes to our constructor to reflect the new changes.

package com.donhamiltoniii.literateoctodollop.controllers;

import java.util.Optional;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.donhamiltoniii.literateoctodollop.models.Author;
import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.AuthorRepository;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Controller
@RequestMapping("/books")
public class BookController {

	@Resource
	AuthorRepository authorRepo;

	@Resource
	BookRepository bookRepo;

	@GetMapping({ "", "/", "/index" })
	public String getAllBooks(Model model) {
		model.addAttribute("books", bookRepo.findAll());
		model.addAttribute("authors", authorRepo.findAll());
		return "books/all";
	}

	@GetMapping("/{id}")
	public String getBook(@PathVariable Long id, Model model) throws Exception {
		Optional<Book> bookOptional = bookRepo.findById(id);
		if (bookOptional.isPresent()) {
			model.addAttribute("book", bookOptional.get());
		} else {
			throw new Exception("The requested book doesn't exist");
		}
		return "books/single";
	}

	@PostMapping({ "", "/", "/index" })
	public String addBook(String title, Long authorId, String genre) {
		Author author = authorRepo.findById(authorId).get();

		bookRepo.save(new Book(title, genre, author));

		return "redirect:/books";
	}
}

Again, this is as simple as changing the order of the values in our constructor.


Back to the Initializer

So now that we have the ability to add multiple Authors to a Book. Let’s add the rest of the missing Authors to our Books.

package com.donhamiltoniii.literateoctodollop;

import javax.annotation.Resource;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;

import com.donhamiltoniii.literateoctodollop.models.Author;
import com.donhamiltoniii.literateoctodollop.models.Book;
import com.donhamiltoniii.literateoctodollop.repositories.AuthorRepository;
import com.donhamiltoniii.literateoctodollop.repositories.BookRepository;

@Service
public class Initializer implements CommandLineRunner {

	@Resource
	AuthorRepository authorRepo;

	@Resource
	BookRepository bookRepo;

	@Override
	public void run(String... args) throws Exception {
		Author rodJohnson = authorRepo.save(new Author("Rod", "Johnson"));
		Author juergenHoeller = authorRepo.save(new Author("Juergen", "Hoeller"));

		Author craigWalls = authorRepo.save(new Author("Craig", "Walls"));

		Author ericFreeman = authorRepo.save(new Author("Eric", "Freeman"));
		Author bertBates = authorRepo.save(new Author("Bert", "Bates"));
		Author kathySierra = authorRepo.save(new Author("Kathy", "Sierra"));
		Author elisabethRobson = authorRepo.save(new Author("Elisabeth", "Robson"));

		bookRepo.save(new Book("J2EE Development without EJB", "Tech", rodJohnson, juergenHoeller));
		bookRepo.save(new Book("Spring Boot In Action", "Tech", craigWalls));
		bookRepo.save(
				new Book("Head First Design Patterns", "Tech", ericFreeman, bertBates, kathySierra, elisabethRobson));
	}

}

Now we have all of our credited Authors! Let’s take a look at how this comes out in the browser!


Uh oh…

We’re getting a 500 status error when we hit /books. That’s because we’re still trying to get an author from our Books. This is a pretty easy fix.

<!doctype html>
<html xmlns:th="https://thymeleaf.org">
    <head>
        <title>Literate Octo Dollop: Books</title>
    </head>
    <body>
        <header>
            <section>
                <h1>Literate Octo Dollop</h1>
                <h2>Books</h2>
            </section>
            <nav>
                <ul>
                    <li><a href="/">Home</a></li>
                    <li><a href="/books">Books</a></li>
                    <li><a href="/authors">Authors</a></li>
                </ul>
            </nav>
        </header>
        <main>
            <ul>
                <li th:each="book : ${books}">
                    <h3><a th:href="@{|/books/${book.id}|}" th:text="${book.title}">Default Title</a></h3>
                    <p>by:</p>
                    <h4 th:each="author : ${book.authors}"><a th:href="|/authors/${author.id}|" th:text="|${author.firstName} ${author.lastName}|">Default Author</a></h4>
                    <small th:text="${book.genre}">Default Genre</small>
                </li>
            </ul>
            <form action="/books" method="POST">
                <label>Title: <input type="text" name="title" /></label>
                <label>
				  Author: 
				  <select name="authorId">
				    <option th:each="author : ${authors}" th:value="${author.id}" th:text="|${author.firstName} ${author.lastName}|">Author</option>
				  </select>
				</label>
                <label>Genre: <input type="text" name="genre" /></label>
                <button type="submit">Submit</button>
            </form>
        </main>
        <footer>
            <small>&copy; Literate Octo Dollop 2019</small>
        </footer>
    </body>
</html>

So now all we’re doing is printing an individual h4 for all authors related to a Book. We’ll need to make this same change in the single book template.

<!doctype html>
<html xmlns:th="https://thymeleaf.org">
    <head>
        <title th:text="${book.title}">Default Title</title>
    </head>
    <body>
        <header>
            <section>
                <h1 th:text="${book.title}">Default Title</h1>
                <h2 th:each="author : ${book.authors}"><a th:href="|/authors/${author.id}|" th:text="|${author.firstName} ${author.lastName}|">Default Author</a></h2>
            </section>
            <nav>
                <ul>
                    <li><a href="/">Home</a></li>
                    <li><a href="/books">Books</a></li>
                    <li><a href="/authors">Authors</a></li>
                </ul>
            </nav>
        </header>
        <main>
            <small th:text="${book.genre}">Default Genre</small>
            <p>More book info coming soon...</p>
        </main>
        <footer>
            <small>&copy; Literate Octo Dollop 2019</small>
        </footer>
    </body>
</html>

And with that, we have our single books displaying all of their Authors!


Conclusion

This brings us to the end of our walk through! If you’ve made it this far, congrats! You’ve created a fully functional Spring Boot MVC application! Obviously there is much more we could do here, but I’m going to leave that as a challenge for you. We should probably have the ability to add an Author to an existing Book in case we miss one initially. We also need some styling. This default stuff isn’t looking so great… Please share any other updates you might add to this project! Thanks for reading!


Don Hamilton III

Written by Don Hamilton III who lives and works in Columbus, OH teaching folx to program at We Can Code IT. You should follow him on Twitter